如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。
在Vue的官方文档中给了一张生命周期图示,可以看到在mounted
及其之前的阶段做的事我们基本都看过了。今天我们先把文件core/instance/lifecycle.js
中还没有看到的方法看完,然后再把所有的事情串联起来,以便我们对Vue实例的生命周期中发生的事有一个全面的了解。
我没有在文章中大量的贴源码,一定要将源码clone到本地哦。
预备知识
首先要明确一件事,我们所说的 Vue实例 有两种情况,一种是开发中手动创建的Vue实例,通常也被称为 根实例。一种是如果模板中依赖了其他组件,Vue内部会去创建这个组件的实例,所以有的时候也会听到 组件的生命周期 这种说法。举个简单的例子看一下:
1 | Vue.component('child', { |
输出的结果如下:
1 | vm created |
可以看到,都会被执行到,所以咱们统一称为 Vue实例的生命周期 。
再拓展一点,创建一个Vue的子类构造器,在它的选项中也提供钩子函数,看一下调用顺序:
1 | const Suber = Vue.extend({ |
输出的结果如下:
1 | Suber created |
可以再加上个按钮,触发时更新页面,会看到updated
钩子的执行顺序如下:
1 | child component updated |
如果在点击按钮时调用this.$destroy()
,destroyed
钩子的执行顺序如下:
1 | child component 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 | // inline hooks to be invoked on component VNodes during patch |
在Vue.js源码学习 —— createComponent中的最后一小节有说过componentVNodeHooks
,然后详细分析了init
的实现。今天我们再来看一下insert
和destroy
的实现过程。
prepatch
是用于更新组件的钩子,组件的更新涉及的东西比较多,等看observer
部分的代码时,我会单独用一篇总结组件的更新流程。
insert
1 | insert (vnode: MountedComponentVNode) { |
可以看到,对于内部创建的组件实例,mounted
钩子是在init
被调用时触发的。而init
是在patch
的过程中,在元素已经插入DOM中后被调用的:
1 | function patch (oldVnode, vnode, hydrating, removeOnly) { |
destroy
1 | destroy (vnode: MountedComponentVNode) { |
可以看到,内部创建的组件实例会在destroy
中使用Vue实例原型上的$destroy
方法去销毁。而destroy
是在patch
过程中元素被从DOM中移除后调用的:
1 | function removeVnodes (vnodes, startIdx, endIdx) { |
initLifecycle
在看其他方法之前,我们先来看一下Vue实例的属性中与生命周期相关的有哪些。
initLifecycle
函数的部分代码如下:
1 | export function initLifecycle (vm: Component) { |
activateChildComponent/deactivateChildComponent
如果是<keep-alive>
组件,会调用这两个lifecycle.js
的函数使组件处于活跃状态或非活跃状态,而不会销毁组件实例。
1 | export function activateChildComponent (vm: Component, direct?: boolean) { |
两个函数处理的过程非常像,先将当前实例标识为活跃或非活跃,然后再递归调用自身,设置子组件的活跃状态,最后调用activated
或deactivated
钩子。由此可知,一定是子组件的钩子先被执行,然后父组件的钩子再调用。
再看一下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
7function 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 许可协议。转载请注明出处!