vue源码分析系列一:new Vue的初始化过程
import Vue from ‘vue’(作者用的vue-cli一键生成)node环境下import Vue from 'vue'的作用是什么意思?在 NPM 包的 dist/ 目录你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别:具体参考:官网完整版:同时包含编译器和运行时的版本。编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。运行时...
import Vue from ‘vue’(作者用的vue-cli一键生成)
node环境下import Vue from 'vue'
的作用是什么意思?
在 NPM 包的 dist/ 目录你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别:
具体参考:官网
完整版:同时包含编译器和运行时的版本。
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。
运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。
UMD:UMD 版本可以通过
CommonJS:CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js)。
开始看代码:
在利用webpack的Vue项目中,在main.js中通过import Vue from 'vue'
导入的vue包如下图所示。(在node_modules/vue/package.json中配置了main属性)
通过官网的解析,我们可以知道:这个包功能其实是不完整的,只有runtime-only的功能。来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码;基本上就是除去编译器的其它一切。
因为运行时版本相比完整版体积要小大约 30%,所以应该尽可能使用这个版本。如果你仍然希望使用完整版,则需要在打包工具里配置一个别名:
webpack:
module.exports = {
// ...
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js'
}
}
}
或者我们可以直接引入:import Vue from '../node_modules/vue/dist/vue.js'
查看主流程上的源码-vue.runtime.common.js
import Vue from 'vue'
过程中,Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。下面是源码中的初始化函数:
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
我们可以改一下:
setTimeout(function(){
console.log('初始化开始');
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
console.log('初始化结束');
},2000)
然后重新运行npm run dev
初始化不依赖浏览器。
initMixin()
function initMixin (Vue) {
Vue.prototype._init = function (options) {// 在Vue的原型上定义 _init 方法
var vm = this;
// a uid
vm._uid = uid$3++;
var startTag, endTag;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm);
} else {
vm._renderProxy = vm;
}
// expose real self
vm._self = vm;
initLifecycle(vm);// 初始化生命周期
initEvents(vm);// 初始化事件
initRender(vm);// 初始化浏览器渲染方法
callHook(vm, 'beforeCreate');// 初始化beforeCreate钩子
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);// 挂载dom元素
}
};
}
new Vue()
在此之前,我修改了一下这3个入口文件:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>vue-cli</title>
</head>
<body>
<div id="application"></div>
<!-- built files will be auto injected -->
</body>
</html>
App.vue
<template>
<div id="component">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'appy',
data () {
return {
msg: 'vue源码解析',
}
},
}
</script>
main.js
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
Vue.config.performance = true
let vm = new Vue({
el: '#application',
components: { App },
template: '<App/>'
})
console.log('Vue.version', Vue.version)
console.log('vm', vm)
通过initMixin
方法之后,执行 new Vue()
操作,在 Vue构造函数内部,调用了vm原型上的 _init()
方法,在this._init()
方法中我们目前主要关注的是 initState()
方法
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);// 在Vue原型上定义了的方法(initMixin方法中),使构建出来的Vue实例调用
}
initState()
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm); // 初始化数据,并且做数据代理
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initData()
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function' // 在vm 上定义一个 '_data' 属性
? getData(data, vm) // getData()方法返回的就是我们在vue data()方法中定义了的属性和方法
: data || {};
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
"The data property \"" + key + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else if (!isReserved(key)) {
proxy(vm, "_data", key); // 数据代理
}
}
// observe data
observe(data, true /* asRootData */);
}
proxy()
在这个方法中,通过Object.defineProperty()来实现数据劫持,当我们通过 this.xxx 访问我们定义的数据时,其实就是访问的 this._data.xxx,从而达到数据代理的目的-双向数据绑定。
var sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
};
function proxy (target, sourceKey, key) {
// target是vm, sourceKey是'_data', key 是我们定义的data中的键
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
总结
Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,这样的编程思想是非常值得借鉴和学习的。
为了弄清楚模板和数据如何渲染成最终的 DOM,所以各种初始化逻辑我们先不看。在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM,下一节我们来分析 Vue 的挂载过程。
更多推荐
所有评论(0)