Vue构造函数的创建过程 一文中介绍了 Vue构造函数 的创建过程,其中第一个对 Vue构造函数 进行成员添加的就是 initMixin(Vue),该调用内创建了 _init 方法。可以说,Vue实例 的大门就是 _init 方法,因此,我们从这个方法入手,一步一步剖析 vm 是如何生成的。
/*
{{msg}}
*/
// Vue.extend 手动挂载组件
const ExtendUse = Vue.extend({
props: ['msg'],
template: `ExtendUse said: {{msg}}
`,
})
const ChildExtend = new ExtendUse({
propsData: {
msg: 'hello Extend'
}
}).$mount('#extendUse')
// Vue.component 自动挂载组件
const ChildComponent = Vue.extend({
props: ['msg'],
template: `child's father said: {{msg}}
`,
})
Vue.component('child-component', ChildComponent)
// Vue 实例
const vm = new Vue({
el: '#app',
data: {
msg: 'hello Vue',
}
})
按照惯例,我先将 _init 的源码简写一下,并且划分一下步骤:
Vue.prototype._init = function (options) {
// 步骤 - 1
const vm = this
vm._uid = uid++
vm._isVue = true
// 步骤 - 2
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 步骤 - 3
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
// 步骤 - 4
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 步骤 - 5
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在这段代码中,我们首要了解的是 vm 指向了谁?
从它的调用 function Vue(options) { this._init(options) }
中我们可以了解到,Vue 是一个构造函数,而 _init 方法又不是该构造函数的静态方法,因此 this 指向了实例,所以 const vm = this
这一步其实就是将实例本身赋值给了 vm。
再具体点?const app = new Vue(options)
这里的 app 就是 vm 的值。
接着,在实例身上添加上 _uid 和 _isVue 的标识,记录当前实例是第 (_uid + 1) 个 Vue 实例对象。
这段代码是一个条件判断,判断的是是否是一个组件。那怎样的存在算是一个组件呢?Vue.extend 手动挂载上去的算不算一个组件呢?
我就先兜个底,只有存在于 vm.options.components 中的才算组件,且能进入 if 判断之中。这就意味着,要么 Vue.component(id, definition)
定义的对象,要么 new Vue({ components: { id: definition } })
定义的对象,其他方式都不具备 _isComponent 属性。因此,即使是 Vue.component 方法内同样也调用了的 Vue.extend 方法,在手动挂载时也不算作组件,不具备 _isComponent 属性。
那这段代码的意义是什么呢?简而言之,就是将 Vue构造函数 中的成员变量 options 和我们传入的 options 合并然后挂载到实例的 $options 属性上。
不知道大家想没想过,组件为什么会进到 _init 中来,哪儿定义了它也可以进来进行初始化操作?
如果你没有忘记 详解Vue.component和Vue.extend 一文中讲过,Vue.component 中会调用一次 Vue.extend,将生成的实例存入 vm.options.components 之中。而在 Vue.extend 中我们曾定义过一个 VueComponent构造函数,这个构造函数继承了 Vue构造函数,因此也有 _init 方法,在 VueComponent构造函数 的 constructor 中又恰好调用了 this._init(options)
,所以套了一层又一层,剥丝抽茧后你应该就能明白为什么组件可以进来了吧。
那么,这个方法内做了些什么?当你进入函数体你会发现,options 中哪来的这么多属性???我丢,见都没见过啊,一脸懵逼逐渐变成N脸懵逼。所以我们先按下不表,因为其中涉及到模板编译部分,扯得太远回不来就麻烦了。
既然不讲组件的 options 合并,那总得讲讲 new (Vue.extend(extendOptions))(options) / new Vue(options)
的 options 合并啊,讲讲讲,这不就来了么。先声明一下,以下 vm 指的是 Vue构造函数 的实例,componentVM 指的是 Vue.extend 返回的构造函数的实例。
这个方法本质上是为了得到 Vue构造函数 身上的 options。
如果传入的 constructor 没有 super 属性,则说明当前实例是 vm,直接返回 vm.constructor.options。
如果传入的 constructor 拥有 super 属性,则说明当前实例是 componentVM,那么当前的 vm.constructor.options 就是 VueComponent.options。如果 VueComponent.superOptions 和 VueComponent.super.options 不相等时会将 VueComponent.superOptions 更新,得到最新的 VueComponent.options 然后返回,否则直接返回 VueComponent.options。
但是,明明 superOptions 和 super.options 都是指向的 Vue构造函数,什么时候会不相等呢?大家可以亲自调试一下以下代码:
const Score = Vue.extend({
template: '{{firstName}} {{lastName}}: {{score}}
'
})
Vue.mixin({
data() {
return {
firstName: 'Walter',
lastName: 'White'
}
}
})
Score.mixin({
data: function () {
return {
score: '99'
}
}
})
new Score().$mount('#app')
首先,我们需要了解这个方法做了什么。
function mergeOptions (parent, child, vm) {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
判断 (vm | componentVM).options.components: { id: definition } 的 id 是否等于 slot / component 或其他原生 HTML 标签,如果是则报错。
当 (vm | componentVM) 中传入的 options 中存在 props 时激活该方法,目的是为了格式化 props 中的属性。
如果 options.props 是一个数组,则将 options.props 的值统一成 { item: { type: null } }。
如果 options.props 是一个对象,则将 options.props 的值统一成 { key: { type: value } }。
当 key 是以短横线命名法( name-space )命名时将其转换成驼峰命名法( nameSpace )
// 如果 options.props 是一个数组
new Vue({
props: ['msg', 'aeo-rus']
})
/*
props: {
msg: {
type: null
},
aeoRus: {
type: null
}
}
*/
// 如果 options.props 是一个对象
new Vue({
props: {
msg: {
type: String,
default: 'hello Vue'
},
'aeo-rus': {
default: 'aeorus'
},
}
})
/*
props: {
msg: {
type: String,
default: 'hello Vue'
},
aeoRus: {
default: 'aeorus'
}
}
*/
当 (vm | componentVM) 中传入的 options 中存在 inject 时激活该方法,目的是为了格式化 inject 中的属性。
如果 options.inject 是一个数组,则将 options.inject 的值统一成 { item: { from: item } }。
如果 options.inject 是一个对象,则将 options.inject 的值统一成 { key: { from: value } } ( 当 value 也是对象时则统一成 { from: key, value.k: value.v } )。
new Vue({
inject: ['onload', 'reload']
})
/*
inject: {
onload: {
from: 'onload'
},
reload: {
from: 'reload'
}
}
*/
new Vue({
inject: {
onload: {
from: 'onlaunch',
default: 'onload'
},
reload: 'reload'
}
})
/*
inject: {
onload: {
from: 'onlaunch',
default: 'onload'
},
reload: {
from: 'reload'
}
}
*/
当 (vm | componentVM) 中传入的 options 中存在 directives 时激活该方法,目的是为了格式化 directives 中的指令。
将 options.directives 的值统一成 { key: { bind: value, update: value } } ( 有可能 value 是一个方法 ) 。
new Vue({
directives: {
focus: {
inserted(el) {
el.focus()
}
}
}
})
/*
directives: {
focus: {
inserted: el => {
el.focus()
}
}
}
*/
new Vue({
directives: {
focus: el => {
el.focus()
}
}
})
/*
directives: {
focus: {
bind: el => {
el.focus()
},
update: el => {
el.focus()
}
}
}
*/
当 (vm | componentVM) 中传入的 options 中存在 extends / mixins 时进入条件判断,递归 mergeOptions 方法,将 extends / mixins 的对象 ( 实质上就是 options ) 与 (vm | componentVM) 进行合并。
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
这个方法极其复杂,本质上就是合并了构造函数身上的属性和 options 上的属性。根据 key 的不同判断是返回 options 上的属性抑或以构造函数身上的属性的值为原型创造的新的拥有 options 上的属性的值的对象。
// 比如当 key 为 components 时返回
options = {
components: {
ChildComponent,
__proto__: {
components: {
KeepAlive,
Transition,
TransitionGroup,
},
},
},
}
我们可以看到在这段代码中多了一个新的属性 _renderProxy,从字面意义上来看应该称之为渲染代理对象,事实上它确实参与了渲染流程,在调用 render 方法获取 vnode 时会将 render 方法的内部指针指向它。
但随之而来产生了两个问题: 1.为什么要有渲染代理对象?2.为什么开发环境和生产环境要用不同的渲染代理对象?
其实并不复杂,我们通过以下代码来进行解答:
/*
目录: core/instance/render.js
vnode = render.call(vm._renderProxy, vm.$createElement)
*/
render(h) {
h('div', 'hello vue')
}
我们通过以上代码可以发现,h函数 其实就是 vm.$createElement,但是因为调用的时候是直接使用 h()
来调用的,因此它内部的指针应该指向 window 而不是当前实例,所以我们需要通过 call 的方式将内部指针指向当前实例,如此就可以使用 this 获取到该实例身上 options 中其他的属性了。
那么开发环境和生产环境为什么要用不同的渲染代理对象呢?我们可以发现在开发环境中其实是调用了 initProxy 方法创建的渲染代理对象,其中判断了是否存在 Proxy 对象,通过 Proxy 对象拦截 this.xxx
中的 xxx 是否是一个非法的属性,有利于我们开发时的操作不谨慎。但是生产环境时就没必要了,毕竟我们不会将错误保留到线上。
这又是一连串的调用,像极了 core/global-api/index.js 中的操作,但是请不要忽略入参,core/global-api/index.js 中传入的是 Vue,而这里传入的是 vm。这意味着,core/global-api/index.js 中是对 Vue构造函数 进行成员的添加,而这里是对 vm 进行属性的添加。
初始化一系列属性,如果当前实例是组件则将 $parent / $root 绑定上组件所归属的 vm。
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options,
_renderProxy,
_self,
/* new add start */
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
/* new add end */
}
初始化事件中心,如果当前实例是组件则会对父组件的事件监听进行重新绑定。
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options,
_renderProxy,
_self,
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
/* new add start */
_events: {},
_hasHookEvent: {},
/* new add end */
}
初始化存放和生成 虚拟DOM 的属性和方法。
如果当前实例不是组件,则将 $attrs / $listeners 设置为空对象添加到实例上。
如果当前实例是组件,则会将父组件 虚拟DOM 上的 attrs 添加响应式后放到自身实例的 $attrs 属性上;再将父组件的事件监听添加响应式后放到自身实例的 $listeners 属性上。
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options,
_renderProxy,
_self,
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
_events: {},
_hasHookEvent: {},
/* new add start */
$vnode: null,
_vnode: null,
$slots,
$scopedSlots,
_c() {},
$createElement() {},
$attrs,
$listeners,
/* new add end */
}
调用 beforeCreate 生命周期。
由于 步骤 - 2 -> resolveConstructorOptions -> mergeOptions -> normalizeInject 这一过程中在实例的 $options 中挂载了 inject 这个属性的缘故,这个方法中就不需要再进行添加,只是单纯地为 inject 中的对象添加了响应式。
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options: {
/* update start */
inject: {}
/* update end */
},
_renderProxy,
_self,
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
_events: {},
_hasHookEvent: {},
$vnode: null,
_vnode: null,
$slots,
$scopedSlots,
_c() {},
$createElement() {},
$attrs,
$listeners,
}
这一步大家肯定熟,只要在网上看过 Vue 源码解析啊响应式原理啊之类视频的应该都了解,这里就是网传的 Vue 的 constructor 中的内容。
即初始化 props / methods / data / computed / watch。
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options: {
inject: {}
},
_renderProxy,
_self,
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
_events: {},
_hasHookEvent: {},
$vnode: null,
_vnode: null,
$slots,
$scopedSlots,
_c() {},
$createElement() {},
$attrs,
$listeners,
/* new add start */
_watchers,
_props, // 如果 options 上有 props
...options.methods, // 如果 options 上有 methods
...options.data, // 如果 options 上有 data
_data: {
...options.data
}, // 如果 options 上有 data
...options.computed, // 如果 options 上有 computed
/* new add end */
}
vm = {
__proto__: {
...Vue,
},
_uid,
_isVue,
$options: {
inject: {}
},
_renderProxy,
_self,
$parent,
$root,
$children: [],
$refs: {},
_watcher: null,
_inactive: null,
_directInactive: false,
_isMounted: false,
_isDestroyed: false,
_isBeingDestroyed: false,
_events: {},
_hasHookEvent: {},
$vnode: null,
_vnode: null,
$slots,
$scopedSlots,
_c() {},
$createElement() {},
$attrs,
$listeners,
_watchers: [], // 如果 options 上有 watch 则会存在 Watcher 的实例
_props, // 如果 options 上有 props
...options.methods, // 如果 options 上有 methods
...options.data, // 如果 options 上有 data
_data: {
...options.data
}, // 如果 options 上有 data
...options.computed, // 如果 options 上有 computed
/* new add start */
_provided, // 如果 options 上有 provide
/* new add end */
}
调用 created 生命周期。
到目前为止,我们都只是初始化的工作,大部分都是挂载某某属性,要说真的做了什么业务相关的事情,那大概就是 initState 这部分了,其中为数据添加了响应式,当有计算属性和侦听器时还顺便做了依赖收集。
但是,我们是否还没看到对于 DOM 的处理?对了,这就是为什么在 beforeCreate 和 created 生命周期里无法获取 this.$refs
的原因,我们还没有进入模板编译,那么 DOM 自然就还没有生成,没有生成的东西怎么可能在这些钩子里被获取呢?
自此,_init 就告一段落了,接下来我们即将进入下一个流程 ———— 模板编译 -> ゲットスタート ( get start )。