Vue.js源码学习 —— Vue实例的生命周期

如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。

在Vue的官方文档中给了一张生命周期图示,可以看到在mounted及其之前的阶段做的事我们基本都看过了。今天我们先把文件core/instance/lifecycle.js中还没有看到的方法看完,然后再把所有的事情串联起来,以便我们对Vue实例的生命周期中发生的事有一个全面的了解。

我没有在文章中大量的贴源码,一定要将源码clone到本地哦。

预备知识

首先要明确一件事,我们所说的 Vue实例 有两种情况,一种是开发中手动创建的Vue实例,通常也被称为 根实例。一种是如果模板中依赖了其他组件,Vue内部会去创建这个组件的实例,所以有的时候也会听到 组件的生命周期 这种说法。举个简单的例子看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vue.component('child', {
// 组件实例的钩子
created () {
console.log('child component created')
},
mounted () {
console.log('child component mounted')
},
render (h) {
return h('p', 'Hello World')
}
})

const vm = new Vue({
el: '#app',
// 根实例的钩子
created () {
console.log('vm created')
},
mounted () {
console.log('vm mounted')
},
render (h) {
return h('child')
}
})

输出的结果如下:

1
2
3
4
vm created
child component created
child component mounted
vm mounted

可以看到,都会被执行到,所以咱们统一称为 Vue实例的生命周期

再拓展一点,创建一个Vue的子类构造器,在它的选项中也提供钩子函数,看一下调用顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Suber = Vue.extend({
created () {
console.log('Suber created')
},
mounted () {
console.log('Suber mounted')
}
})

const vm = new Suber({
el: '#app',
created () {
console.log('vm created')
},
mounted () {
console.log('vm mounted')
},
render (h) {
return h('child')
}
})

输出的结果如下:

1
2
3
4
5
6
Suber created
vm created
child component created
child component mounted
Suber mounted
vm mounted

可以再加上个按钮,触发时更新页面,会看到updated钩子的执行顺序如下:

1
2
3
child component updated
Suber updated
vm updated

如果在点击按钮时调用this.$destroy()destroyed钩子的执行顺序如下:

1
2
3
child component destroyed
Suber destroyed
vm destroyed

总结一下Vue实例的生命周期钩子的执行顺序:

- created:先调用构造器的,再调用根实例的,最后调用子组件实例的。

- mounted/updated/destroyed:先调用子组件实例的,再调用构造器的,最后调用根实例的。

componentVNodeHooks

componentVNodeHooks是一个哈希表,位于文件vdom/create-component.js中,里面放的钩子函数会在Vue内部创建组件实例时,合并到vnode.data.hook中(开发者在创建vnode时也可以在vnode.data.hook中提供钩子函数)。vnode.data.hook中的钩子函数又在patch的过程中被调用。componentVNodeHooks的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
// ...
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// ...
},
insert (vnode: MountedComponentVNode) {
// ...
},
destroy (vnode: MountedComponentVNode) {
// ...
}
}

Vue.js源码学习 —— createComponent中的最后一小节有说过componentVNodeHooks,然后详细分析了init的实现。今天我们再来看一下insertdestroy的实现过程。

prepatch是用于更新组件的钩子,组件的更新涉及的东西比较多,等看observer部分的代码时,我会单独用一篇总结组件的更新流程。

insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
// 还没有被挂载的话,直接调用 mounted 钩子
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// 等看`observer`部分的代码时再详细看下面这个函数
queueActivatedComponent(componentInstance)
} else {
// <keep-alive>组件再次激活组件
activateChildComponent(componentInstance, true /* direct */)
}
}
}

可以看到,对于内部创建的组件实例,mounted钩子是在init被调用时触发的。而init是在patch的过程中,在元素已经插入DOM中后被调用的:

1
2
3
4
5
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}

destroy

1
2
3
4
5
6
7
8
9
10
11
12
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
// 不是<keep-alive>组件,直接调用$destroy销毁实例
componentInstance.$destroy()
} else {
// 如果是<keep-alive>组件,让组件处于非活跃状态
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}

可以看到,内部创建的组件实例会在destroy中使用Vue实例原型上的$destroy方法去销毁。而destroy是在patch过程中元素被从DOM中移除后调用的:

1
2
3
4
5
6
function removeVnodes (vnodes, startIdx, endIdx) {
// ...
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
// ...
}

initLifecycle

在看其他方法之前,我们先来看一下Vue实例的属性中与生命周期相关的有哪些。

initLifecycle函数的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function initLifecycle (vm: Component) {
// ...省略找到第一次非抽象parent的过程

// 父实例,根 Vue 实例没有,子组件实例有
vm.$parent = parent
// 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
vm.$root = parent ? parent.$root : vm

// 当前实例的直接子组件。
vm.$children = []
// 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
vm.$refs = {}

// 在内部使用的一些变量
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}

activateChildComponent/deactivateChildComponent

如果是<keep-alive>组件,会调用这两个lifecycle.js的函数使组件处于活跃状态或非活跃状态,而不会销毁组件实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function activateChildComponent (vm: Component, direct?: boolean) {
// ...
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}

export function deactivateChildComponent (vm: Component, direct?: boolean) {
// ...
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}

两个函数处理的过程非常像,先将当前实例标识为活跃或非活跃,然后再递归调用自身,设置子组件的活跃状态,最后调用activateddeactivated钩子。由此可知,一定是子组件的钩子先被执行,然后父组件的钩子再调用。

再看一下direct参数的作用,在insert中和destroy中调用的时候传了该标识为true,但是在递归调用时没有传。是因为一开始需要判断vm是否已经在非活跃状态的树中,如果是的话直接返回了。

为什么要这样判断我暂时还没有找到答案,后面找到答案了再回来补上-.-

Vue.prototype.$destroy

Vue.prototype.$destroy是公开给开发者使用的实例方法,从它的实现上可以看到在销毁一个实例时都做了哪些事情,主要如下:

  • 触发beforeDestroy钩子,通知开发者实例即将要被销毁。

  • 将当前实例从其父实例的children中移除掉。

  • 移除实例的所有观察者。

  • 标识实例已销毁,并调用__patch__触发在渲染树中的destroy钩子函数,同时也销毁当前实例的子实例。

    __patch__的传参中vnode传的是null,在进入patch函数中会执行下面这句:

    1
    2
    3
    4
    5
    6
    7
    function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
    }
    // ...
    }

    invokeDestroyHook中就会触发directives模块和ref模块的destroy了,进而将所有指定解绑,也将实例从$refs中移除掉。

    因为在invokeDestroyHook也会子节点递归调用该方法,所以子实例也会一起被销毁。

  • 触发destroyed钩子,通知开发者实例已经被销毁。

  • 移除所有实例的事件监听器。

  • 移除其他关联的引用。

生命周期图示

将所有已经看过的内容画了一张图,图片有点大,耐心等待一下啦~

下一篇文章将开始看新的模块啦,Vue中数据响应系统的设计。

本文作者:意林
本文链接:http://shinancao.cn/2020/01/27/Vue-Source-Code-09/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!