Vue 的基本的结构我们已经弄清楚了,下面正式学习 Vue 的实例对象初始化过程。
现在我们回到出生地文件core/instance/index.js,查看下面的代码。
import { initMixin } from "./init";
// ...
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);
}
initMixin(Vue);
// ...在使用new的创建实例对象的过程中,会执行构造函数中的this._init()方法,并且把选项参数options传入,这就是 Vue 的初始化方法。
_init初始化方法#
这个方法是通过initMixin(Vue)函数添加到 Vue 的构造函数的原型对象中的,现在打开core/instance/init.js文件,看下面的代码。
let uid = 0
export function initMixin(Vue: Class<Component>) {
// ! 初始化的方法
Vue.prototype._init = function(options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++ // ! 唯一标示
// ...
}_init方法中首先使用常量vm来缓存this,但是这个this到底指向哪里呢?
我们知道在构造函数中,是使用this._init()调用的这个方法的,而且是在使用new创建实例的时候调用的,那么这个this很明显应该指向的是创建的实例对象。
然后往实例对象中添加唯一标识_uid,它是一个常量,初始值是0,它的值是递增的 ,所以每一个实例对象的_uid值都是不一样的。
继续看下面的代码。
let 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); // ! 性能追踪前标记
}
// ...
/* 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);
}上面代码的省略了中间的主要逻辑代码,只保留了两处代码。这两处代码是非生产环境时使用的,用来测试中间代码的性能。
接下来查看省略的中间部分的代码,这才是_init主要的逻辑代码。
先看代码的前面部分。
// 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), // ! Vue 初始化时的默认配置,比如默认的指令和组件等等
options || {}, // ! 用户传入的配置
vm // ! Vue 实例对象本身
);
}首先定义了_isVue属性,这个属性是一个布尔值,用来标识当前的对象是 Vue 的实例对象。然后通过选项的_isComponent属性判断这是不是一个组件,如果是组件就调用initInternalComponent函数初始化这个组件。
在创建实例对象的过程中,根本就没有_isComponent这个属性,所以现在应该执行else块中的代码,调用mergeOptions函数合并选项(配置),把 Vue 的默认选项和用户输入的选项进行合并,这里需要把实例对象也传入进去。
关于 Vue 的默认选项,还记得在 core 增加静态属性和方法时,遍历ASSET_TYPES数组时初始化了Vue.options中的几个空对象吗?它们分别是components、directives、filters,然后把内置组件<keep-alive />组件放入到components中。
后面在 Runtime 入口文件对构造函数进行包装时,也设置了 Web 平台的config属性,还增加两个指令到directives, 增加了两个过渡动画组件到components中,其实这些就是 Vue 的默认选项。
这里通过resolveConstructorOptions函数来获取这些默认选项。
用户选项就是我们创建 Vue 实例时输入的选项,比如在项目入口文件时使用new创建根组件时传入的对象就是根实例的选项配置。
new Vue({
el: "#app",
render: (h) => h(App),
});另外,我们在 Vue 组件中的script标签导出的对象也是实例组件的选项配置。
现在 Vue2 的 API 其实就是选项(options) API,所以合并选项是 Vue 源码中非常重要的内容,后面会单独分析。
继续查看下面的代码。
/* istanbul ignore else */
// ! 初始化代理
if (process.env.NODE_ENV !== "production") {
initProxy(vm);
} else {
vm._renderProxy = vm;
}在非生产环境中会调用initProxy方法并传入 Vue 的实例对象,而在生产环境中是直接给实例赋值属性_renderProxy为vm。现在虽然不知道initProxy方法的内容,但是我们可以判断它的功能应该也是为实例属性_renderProxy赋值,因为生产环境和开发环境的代码实现的结果应该是一致的。
继续查看后面的代码。
// expose real self
vm._self = vm;
initLifecycle(vm); // ! 初始化生命周期相关配置 存储本身实例到父节点 新增属性 $parent $root $children $refs 等
initEvents(vm); // ! 初始化事件相关配置 更新 listeners
initRender(vm); // ! 初始化渲染, 创建VNode 另新增属性 $attrs 和 $listeners
callHook(vm, "beforeCreate"); // ! 调用 beforeCreate 钩子函数
initInjections(vm); // ! 初始化 Injections resolve injections before data/props
initState(vm); // ! 初始化状态 按顺序 props => methods => data => computed => watch
initProvide(vm); // ! 初始化 Provide resolve provide after data/props
callHook(vm, "created"); // ! 调用 created 钩子函数上面函数中的函数名很多是以init开头的,因为这些函数才是真正初始化实例的函数。
大概查看了一下这些函数,发现它们也是为实例对象vm添加或者更新各种属性或者方法的,但是和前面为构造函数的原型对象添加属性和方法时不同。
这些函数都有一个共同点:那就是需要传入实例对象vm,并且会用到vm.$options这个属性。这个属性是合并后的选项,借助这个属性为实例对象vm再次添加或者更新属性或者方法。比如我们在options设置了data: { name: 'Hale'},那么在初始化后会把这个属性放入到实例的_data属性中,并且还会把它变成响应式数据。
这里还有两个是调用了callHook方法,这是去执行生命周期钩子函数,它们是beforeCreate和created这两个生命周期钩子函数。在options选项中,如果我们设置了这两个生命周期钩子函数,那么就会在这里,按照上面函数的执行顺序来执行。
关于组件中请求数据的时机#
这里简单分析一下请求数据的时机,在实际开发中,我们可能需要在实例组件向后端请求接口数据,然后保存到data属性中,让他们变成响应式数据。
从上面的代码中,我们发现beforeCreate生命周期函数钩子执行的时机,是在initState之前执行,而在这个时候 Vue 实例组件中还没有初始化state,所以不适合在这个钩子函数里面去请求数据并赋值给data属性。
而created生命周期函数钩子是在最后面的时候执行的,也就是在实例初始化完成之后执行。这个时候就可以去请求数据的,因为这是state应该初始化完成了,在这个钩子函数里面请求后的数据赋值给data后也会变成响应式的数据。
另外也还可以在mounted生命周期函数钩子中请求数据,这个钩子在挂载组件后执行,但是它没有在created钩子中请求的时机那么快。