从源码的角度分析一下vue2中生命周期函数的执行顺序问题
提出问题
本文目标是:在源码的角度解决下面的三个问题
- 初次渲染的时候父子组件生命周期函数执行顺序是怎样的?
- 子组件发生更新的时候生命周期函数的执行顺序是怎样的?
- 在销毁父组件的时候父子组件生命周期函数又是如何执行?
在源码当中new Vue的时候首先执行的是Vue这个构造函数
而构造函数中又调用了一个叫_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)
}
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = 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)
}
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// 调用 beforeCreate 生命周期钩子
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm) // resolve provide after data/props
// 调用 create 生命周期钩子
callHook(vm, 'created')
/* 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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在我注释的地方可以看到首先执行的是beforeCreate和Created生命周期函数,值得注意的是在vue当中,每个组件都会执行_init方法(这里先知道这一点即可),他是一个递归的过程。
所以如果是首次渲染,那么先执行父组件的beforeCreate和Created生命周期函数。
在_init方法的下面有一句代码
vm.$mount(vm.$options.el)
执行这句代码后面会调用一个叫mountComponent的方法
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 挂载 beforeMount 生命周期钩子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 在再次更新渲染之前调用beforeUpdate钩子
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// 最后调用 mounted 生命周期钩子函数
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
在我注释的地方可以看到执行了beforeMount生命周期函数,但是需要注意的是,在调用Mounted生命周期函数之前执行了new Watcher()这句代码,从此父组件的Mounted生命周期函数暂时停滞了,等待所有子组件都执行完成了在执行,所以最顶层的父组件他们mounted生命周期函数必定是最后执行。
那new Watcher以后干了什么呢?答案是递归执行了子组件的_init方法(这里先不展开细说,涉及到很多内容)
所以根据上面的逻辑子组件会先后执行beforeCreate、Created、beforeMount这三个方法
我们先注意mountComponent的方法中最后的这句代码
// 最后调用 mounted 生命周期钩子函数
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
其中vm.$vnode保存的是该组件实例对应的父组件的虚拟dom,所以很明显vue的实例是不存在父组件的,所以只有new Vue的时候才会在此处执行mounted生命周期函数,那普通组件的mounted生命周期在哪里执行呢,看下面文章
在最底层的子组件完成dom创建和插入以后,就会去调用一个叫invokeInsertHook的方法,该方法在patch函数的最下方(如果不知道patch可以先不用管它是什么),该方法会执行一个叫insert的钩子方法
insert(vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
// 子 凡是组件都是在这里调用mounted new Vue实例是在mountComponent方法里面执行mounted
callHook(componentInstance, "mounted");
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true /* direct */);
}
}
},
而patch方法是在mountComponent方法中的new Watcher以后执行的,所以它是在beforeMount方法执行之后执行
而在patch阶段子组件也会产生递归的,它会将上一个父组件的Mounted生命周期函数停滞也就是将invokeInsertHook方法停滞,而去执行到_init这个方法,等待子组件执行完以后跳出递归才能执行,所以和上面的道理一样,父组件的Mounted生命周期函数必定是等到所有子组件执行完mounted以后再执行,所以由于最底层的子组件已经不需要递归了所以直接执行invokeInsertHook方法,然后跳出递归,轮到上一个父组件执行invokeInsertHook方法,所以计算下来永远是子组件先执行mounted生命周期函数。
此时可以解答第一个问题了
所以得出结论,在父组件嵌套子组件的时候,我们假设现在嵌套3层吧,
分别是父组件a,子组件b,孙组件c
那么执行示意图为
下面看看子组件发生更新的时候beforeUpdate和updated方法是如何执行的
注意本文是研究父子组件的生命周期函数的执行顺序,所以我应该制造一种场景使得在数据更新的时候父子组件的beforeUpdate和updated都会执行,这样才能探究他们的执行顺序(因为一般情况下只更新父组件的data或者是子组件的data只会触发父组件或者是子组件其中一个组件的beforeUpdate和updated)
场景代码如下:
先看父组件
<template>
<div id="app">
<test>{{name}}</test>
<button @click="handler">父组件更新按钮</button>
</div>
</template>
<script>
import test from './components/test_mixins_and_extends.vue'
export default {
name: 'App',
beforeUpdate() {
console.log('父组件的beforeUpdate')
},
updated() {
console.log('父组件的Updated')
},
data(){
return {
name: '小何'
}
},
methods: {
handler() {
this.name = '小何程序员'
}
},
components: {
test
}
}
</script>
父组件定义了两个更新的生命周期函数,同时注册了子组件test,利用插槽向子组件传入了数据
下面看看子组件的代码
<template>
<div>
这个是父组件传进来的数据:<slot></slot>
<br/>
<button @click="update">子组件更新按钮</button>
这个是子组件的数据:{{name}}
</div>
</template>
<script>
export default {
data(){
return {
name: "小河"
}
},
beforeUpdate() {
console.log('子组件的beforeUpdate')
},
updated() {
console.log('子组件的Updated')
},
methods: {
update(){
this.name = '小河程序员'
}
},
}
</script>
子组件也是定义了两个更新的生命周期函数,同时利用slot标签接受了父组件传过来的数据
渲染的结果为
点击子组件按钮,毫无疑问先执行子组件的beforeUpdate再执行子组件的updated,但是当点击父组件按钮的时候却是如下这样
那为什么会这样呢?
我们分析一下源码
首先vue2是先进行父组件的初始化工作然后再进行子组件的,这在上面的分析已经可以知道。
所以在编译父组件的template的时候读取了this.name这个data中的数据,由于data中的数据都是具有响应式的,所以访问这些数据的时候就会对当前的渲染watcher进行依赖的收集(如果不熟悉这一块知识,只需要知道这里的watcher相当于是一个存放更新页面的方法的一个容器同时也存在着beforeUpdate和updated生命周期函数,每一个组件都有对应的渲染watcher,而依赖收集的作用就是收集哪些watcher访问了这个数据,当数据发生改变的时候就能够找到对应的watcher,执行watcher里面的方法就能更新该组件并且重新刷新页面,使得页面显示的内容是最新的)
也就是说在修改这个name的时候就会触发父组件的beforeUpdate和updated生命周期函数
下面是执行beforeUpdate和updated生命周期函数的位置
function flushSchedulerQueue() {
currentFlushTimestamp = getNow();
flushing = true; // 将 flushing 置为 true,代表正在刷新队列
let watcher, id;
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id);
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
// 这个before方法里面执行了beforeUpdate生命周期函数
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== "production" && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
"You may have an infinite update loop " +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
);
break;
}
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice();
const updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
// 触发 update 生命周期
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit("flush");
}
}
在注释位置可以看到watcher.before()
这个方法里面就是执行了这个父组件的beforeUpdate生命周期函数
而下面的watcher.run();
则是更新页面的操作,神奇的地方来了
从上面代码可以看出watcher.before()
是放在一个循环里面的,而一开始控制循环的queue.length
是1,但是当执行到watcher.run();
的时候queue.length
变为2,也就是循环还没有结束但是循环次数就增加了1。
增加了1的目的就是为了执行子组件的beforeUpdate生命周期函数,watcher.run();
在更新页面的时候,他会对子组件进行diff算法,由于name数据发生改变子组件前后发生了改变,所以在patch函数当中会执行到下面的这个patchVnode方法
// 1、判断节点是否可以复用,可以复用则对节点打补丁
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 老节点不是真实 DOM 并且新旧 VNode 节点判定为同一节点时会进行 patchVnode 这个过程
// 同一节点代表可复用
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
而首次在patchVnode方法当中对比的是<test>小何</test>
和<test>小何程序员</test>
,这里外层是相同的,但是子节点不相同,所以在patchVnode方法方法当中会执行到下面这个updateChildren方法
...
if (isUndef(vnode.text)) {
// 新节点不是文本节点
if (isDef(oldCh) && isDef(ch)) {
// 新老节点均有 children 子节点,调用 updateChildren 对子节点进行 diff 操作
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if {...}
...
}
但是updateChildren方法里面又递归调用了patchVnode方法
此时将第二次进入patchVnode方法
在第二次进入的时候会有所不同他会执行到下面这个 i 方法
...
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
...
这里的 i 方法根据上面的 if 判断可以知道其实就是prepatch钩子方法,而这个钩子方法里面执行了updateChildComponent方法,并且传入子组件的实例作为执行的上下文,所以下面的vm就是子组件的实例
看看updateChildComponent方法的源码
...
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
...
这句代码的意思是如果存在插槽那么执行$forceUpdate(),这个方法就是强制更新页面(强制执行子组件渲染watcher中的更新页面的方法,源码称为update方法,注意不是updated生命周期函数)
而这个update方法当中调用queueWatcher(this);方法将当前的watcher通过push方法添加到queue当中,此时queue的长度增加了1,到此执行结束,然后返回到原来的父组件中flushSchedulerQueue位置,此时刚刚执行完watcher.run();
,然后再次判断循环条件,发现queue.length值为2,而index++后为1,再次进入循环执行子组件的watcher.before()
方法触发子组件的beforeUpdate生命周期函数。
在跳出循环以后会执行下面这段代码
...
callUpdatedHooks(updatedQueue);
...
在看看这个函数的定义
function callUpdatedHooks(queue) {
let i = queue.length;
while (i--) {
const watcher = queue[i];
const vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
// 执行 updated
callHook(vm, "updated");
}
}
}
就是循环queue,然后从最后一个开始执行updated生命周期函数,而我们刚刚分析已经知道父组件的watcher是最早进入queue的,子组件是最后进入的,所以先执行的应该是子组件的updated生命周期函数,然后再执行父组件的。
此时可以解答第二个问题了
所以得出结论,在父组件插槽子组件的时候,我们假设现在嵌套3层吧,
分别是父组件a,子组件b,孙组件c
那么执行示意图为
对于第三个问题请看下文
首先需要知道的是每一更新页面都会触发patch阶段,这个阶段就是进行diff算法的,这个函数的最顶部有一个判断,就是当新虚拟dom不存在该节点但是老虚拟dom存在的时候,那么就销毁这个节点,在销毁的时候会触发$destroy方法 如下
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
// 调用 beforeDestroy 钩子
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
// 把自身从父组件移除
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// 移除依赖监听
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
// 移除对 data 的响应式处理
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// 为 true 代表实例已经清除完毕
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
// 调用 __patch__,销毁节点
vm.__patch__(vm._vnode, null)
// 调用 destroyed 钩子
callHook(vm, 'destroyed')
// 移除所有事件播报之类的监听
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
在注释的地方可以看到当父组件来到这里的时候先执行的是父组件的beforeDestroy生命周期函数,然后调用vm.patch(vm._vnode, null)销毁节点 但是这个方法存在递归,它会再次执行到patch函数中,再次判断就会进入invokeDestroyHook方法中,所以此时父组件的beforeDestroy生命周期函数调用了以后就停滞了
function invokeDestroyHook(vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
由代码可以看到如果vnode.children存在那么对其子节点递归调用invokeDestroyHook方法进行销毁,然后每一个子节点都会执行上面的i方法,这个i方法里面又执行了上面的$destroy方法,如此返回递归
此时可以解答第三个问题了
所以得出结论,在父组件嵌套子组件的时候,我们假设现在嵌套3层吧,
分别是父组件a,子组件b,孙组件c
那么执行示意图为
好了 到此全部问题都已经解答了,感谢你的阅读,如果有什么不懂或者不妥的地方可以在评论区中指出,谢谢