前言
最近在工作之余也陆陆续续研究了vue2.x的源码,打算通过文章来记录对源码的一些学习收获和总结,本人第一次写文章,有点紧张又有点激动,希望今后能继续坚持下去。这篇是通过源码来分析整个生命周期执行的机制,如果文章有错误不对的地方,欢迎指出,不胜感谢,如果对你有帮助,请为我点个赞吧,谢谢。
前置知识
//子组件
var sub = {
template: '<div class="sub"></div>'
}
//父组件
new Vue({
components: {sub},
template: `<div class="parent">
<sub></sub>
<p></p>
</div>`
})
子组件实例vm{
$vnode 指的是父vnode,即例子里父组件里<sub></sub>这个vnode
_vnode 指的是渲染vnode,即本身渲染的元素,即例子里的<div class="sub"></div>
}
beforeCreate和created钩子
我们从入口分析起,new Vue的时候,会执行原型里的_init的初始化方法。
function Vue (options) {
...
this._init(options);
}
Vue.prototype._init = function (options) {
var vm = this;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);
initProvide(vm);
callHook(vm, 'created');
...
};
我们来看一下他的_init方法,这里简化了一下代码,去掉了跟生命周期无关的,我们看到会在执行了initLifecycle(vm);initEvents(vm);initRender(vm);然后执行了callHook(vm, 'beforeCreate')的方法,这里就触发了vue实例上beforeCreate钩子的执行,我么来看一下callHook的实现,之后所有生命周期的执行,都会通过这个函数传入不同生命周期参数来实现。
function callHook (vm, hook) {
pushTarget();
var handlers = vm.$options[hook];
var info = hook + " hook";
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info);
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook);
}
popTarget();
}
这里我们可以看到他拿到$options定义的生命周期函数数组,进行遍历执行,组件上显性去定义的
vm.$on('hook:xxx',()=>{})
自定义事件也会在这里进行调用执行。 之后就是执行了initInjections(vm); initState(vm);initProvide(vm);这里是对组件上inject,data,props,watches,computed等属性进行响应式绑定后,执行了created的生命周期钩子。
父子组件执行顺序
我们知道,父组件在patch过程中,当遇到组件vnode,组件vnode会执行createComponent方法,然后进行子组件构造函数的实例化,也会执行vue初始化的一整套流程,因为父是先比子创建的,所以执行顺序会是
父beforeCreate > 父created > 子beforeCreate > 子created
beforeMount和mounted钩子
组件在进行cteated之后,要执行$mount(mountComponent)方法,然后执行里面的render和patch方法,进行组件的挂载。
function mountComponent (
vm,
el,
hydrating
) {
...
vm.$el = el;
callHook(vm, 'beforeMount');
var updateComponent;
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
...
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
这里patch之前会执行beforeMount钩子,而这个函数里要执行mounted钩子,是要在vm.$vnode为null的情况下,$vnode我们知道是组件的父组件vnode。但是子组件我们知道都是有$vnode的,那么他会在哪里去触发mounted钩子呢,其实vue的根实例通过createElm创建真实dom时插入文档时,会传入insertedVnodeQueue,在递归过程中去收集子组件实例,然后最后在整个真实dom插入文档后,通过invokeInsertHook来遍历执行子组件的mounted钩子。最后根实例的$vnode为null,所以最后才进行mounted。
function patch (oldVnode, vnode, hydrating, removeOnly) {
let insertedVnodeQueue = []
let isInitialPatch = false
if (子组件初次创建时) { isInitialPatch = true ...} else {
createElm(
vnode,
insertedVnodeQueue,//根实例创建真实dom时,会传入insertedVnodeQueue,收集子组件的实例
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
}
...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
function invokeInsertHook (vnode, queue, initial) {
<!--vnode是渲染vnode,它的parent是它父组件vnode-->
if (isTrue(initial) && isDef(vnode.parent)) {
//因为子组件生成真实Dom后,都会走到这里,当判断为组件为初次渲染且有父vnode
//就不进行遍历queue,而是把队列里保留在data.pendingInsert属性里,供后续父实例拿到当前队列
//只有根实例的时候才会执行遍历insert钩子,即触发所有子组件的mounted钩子。
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
这里介绍一下组件vnode创建过程中会安装一些组件钩子,用于不同时候的调用,这里的data.hook.insert就是组件的真实dom插入时会执行的钩子
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
...
},
prepatch: function prepatch (oldVnode, vnode) {
...
},
<!--插入勾子-->
insert: function insert (vnode) {
var context = vnode.context;
var componentInstance = vnode.componentInstance;
if (!componentInstance._isMounted) {
componentInstance._isMounted = true;
callHook(componentInstance, 'mounted');
}
<!--keep-alive时候调用-->
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance);
} else {
activateChildComponent(componentInstance, true /* direct */);
}
}
},
<!--销毁勾子-->
destroy: function destroy (vnode) {
var componentInstance = vnode.componentInstance;
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
<!--keep-alive时候调用-->
deactivateChildComponent(componentInstance, true /* direct */);
}
}
}
};
这里在父子组件嵌套时,会深度遍历执行patch函数,子组件真实Dom会优先插入到父元素里,所以子组件实例会先插入到insertedVnodeQueue。
父子组件执行顺序
因为patch函数是父先以子执行的,所以beforeMount是父>子,而子组件是优先插入到insertedVnodeQueue队列里,最后在遍历过程,子组件的mmouted会先执行,所以mounted子>父,所以顺序是
父beforeMount > 子beforMount > 子mounted > 父mounted
整体初次渲染的顺序是 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
beforeUpdate和updated钩子
这两个钩子都是在组件更新的时候触发的,在$mount(mountComponent)挂载的时候,还有这样一段代码
function mountComponent() {
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
}
这里是创建组件的渲染watcher,并传入before函数,里面是beforeUpdate钩子的执行。我们知道,当父子组件更新的时候,会根据响应式系统,调用watcher的方法update将watcher push到一个队列里,并会在下一个tick里执行函数flushSchedulerQueue 遍历queue进行更新,执行before函数触发beforeCreate钩子,并通过watcher.vm拿到组件实例,触发updated勾子。
Watcher.prototype.update = function update () {
...
queueWatcher(this);
};
function queueWatcher (watcher) {
...
nextTick(flushSchedulerQueue);
}
function flushSchedulerQueue () {
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
...
}
...
var updatedQueue = queue.slice();
callUpdatedHooks(updatedQueue);
}
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated');
}
}
}
父子组件执行顺序
因为queue队列里排列顺序是父先以子,所以执行before函数时,是父beforeUpdate > 子beforeUpdate,而在callUpdatedHooks时,while循环时,是以最后的watcher递减下来执行callHook(vm, 'updated'),所以总的执行顺序是
父beforeUpdate > 子beforeUpdate > 子updated > 父updated
beforeDestroy和destroyed钩子
这两个钩子都是在组件销毁过程中执行的,在组件更新过程中,会进行新旧vnode的diff算法,逻辑在patchVnode中的updateChildren函数里,具体的逻辑大家可以去源码看看,因为比对中,就会去删除一些没用的节点,就会触发removeVnodes函数,进而会执行invokeDestroyHook函数,去执行组件vnode里的钩子data.hook.destroy(可看一下上面代码安装在组件vnode的勾子有哪些)
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else { // Text node
removeNode(ch.elm);
}
}
}
}
function invokeDestroyHook (vnode) {
var i, j;
var 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的destroy钩子里,会执行componentInstance.$destroy();进而执行到下面Vue原型上挂载的$destroy方法,vm.patch(vm._vnode, null)这个代码会传入vm_vnode和null,vm_vnode即渲染vnode,将其子vnode进行递归执行invokeDestroyHook方法进行销毁
Vue.prototype.$destroy = function () {
var vm = this;
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy');
...
vm.__patch__(vm._vnode, null);
callHook(vm, 'destroyed');
...
};
父子组件执行顺序
因为触发removeVnodes函数,是先父后子的,所以执行实例执行$destroy的时候,是父beforeDestroy > 子beforeDestroy,然后vm.patch(vm._vnode, null)又会递归去寻找他的子组件,去执行data.hook.destroy,所以子组件的destroyed钩子会先执行,父组件后面执行
父beforeDestroy > 子beforeDestroy > 子destroyed > 父destroyed
deactivated和activated钩子
这两个钩子的话是应用在keep-alive组件所包裹的组件下的,跟mounted和destroyed钩子类似,再代码判断里,通过vnode.data.keepLive来区分普通非缓存组件,进而执行不同的钩子
写在最后
这篇是总结了vue10个生命周期运行机制,如果你有幸看完了,如果有什么不对的地方,请评论指出或私自探讨一下,如果觉得不错,点个赞吧。哈哈