三 · To be created or not to be created

有一道颇为经典的面试题是:data在Vue的哪个生命周期开始可以使用?如果有刷过面试题或者实际尝试过的前端可以脱口而出:created。然而真正的问题在于,为什么?

在Vue的文档里给出过一个很有用的生命周期示意图:

但这幅图没有也没必要把一些具体实现告诉你,本来框架的诞生就是为了让开发不用关心这些事情,但我们这里就是一探究竟,在new Vue()的初始化里,都发生了些什么事情。

上一节里我们提到的这个部分:

1
2
3
4
5
6
7
8
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')

在这里初始化了很多东西,第一个函数,望文生义,初始化生命周期,我们追踪到这个函数的内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initLifecycle (vm: Component) {
// 省略部分代码
vm.$parent = parent
vm.$root = parent ? parent.$root : vm

vm.$children = []
vm.$refs = {}

vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}

initLifecycle函数接收一个Vue对象,并给这个对象添加许多属性。我们发现有_isMounted、_isDestroyed、_isBeingDestroyed这几个属性。在initEvents和initRender之后又有一个callhook函数,我们进去能看到:

1
2
3
4
5
6
7
8
9
10
function callHook (vm: Component, hook: string) {
// 省略部分代码
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
}

这个函数接收两个参数,一个是vue实例,不用多说,一个是hook。hook其实就是Vue定义的生命周期,源码里传了个字符串“beforeCreate”进来。那这个有什么用呢?我们看这句:

1
2
3
4
5
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}

handlers去找option里有没有这个属性,其实也就是我们在使用Vue实例时在特定生命周期写的那些函数。如果这个地方存在函数,就会执行invokeWithErrorHandling函数。这个函数大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res)) {
// issue #9511
// reassign to res to avoid catch triggering multiple times when nested calls
res = res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
}
} catch (e) {
handleError(e, vm, info)
}
return res
}

我们先省略一些不是我们需要注意的部分,这个函数就是:

1
2
3
4
5
6
7
8
function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[]
) {
let res
res = args ? handler.apply(context, args) : handler.call(context)
}

这里面关键就在于通过apply和call去执行了我们传进来的handler,其实就是我们在option上定义的生命周期函数。apply和call很接近,一些具体的区别可以参考MDN文档

这样我们就理解这个写法了:

1
2
3
4
5
6
var app = new Vue({
el: 'app',
beforeCreate: function () {
console.log('Before I was created. But if I haven\'t be created, how could I say this?')
}
})

我们在option上定义的生命周期函数就是这样被拿去执行的。我们这样一路看下来,会发现在callhook传入beforeCreate之前,根本没有和data或者method相关的内容,这个在哪里呢,我们来看后面的一个叫initState的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initState (vm: Component) {
vm._watchers = []
const 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了吗?这个函数也在同样的文件里,这里先不用看其中的具体内容了,因为现在我们已经足够可以回答这篇文章开头的题目:在callHook到beforeCreate的时候,也就是我们可以写函数的时候,data和method这些对象还没有被初始化出来,自然也就不可能在这个时候去调用它们啦。

(第三节 · 完)