最近在工做之余也陆陆续续研究了vue2.x的源码,打算经过文章来记录对源码的一些学习收获和总结,本人第一次写文章,有点紧张又有点激动,但愿从此能继续坚持下去。这篇是经过源码来分析整个生命周期执行的机制,若是文章有错误不对的地方,欢迎指出,不胜感谢,若是对你有帮助,请为我点个赞吧,谢谢。javascript
//子组件 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> }
咱们从入口分析起,new Vue的时候,会执行原型里的_init的初始化方法。vue
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的实现,以后全部生命周期的执行,都会经过这个函数传入不一样生命周期参数来实现。java
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定义的生命周期函数数组,进行遍历执行,组件上显性去定义的node
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。dom
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。ide
由于patch函数是父先以子执行的,因此beforeMount是父>子,而子组件是优先插入到insertedVnodeQueue队列里,最后在遍历过程,子组件的mmouted会先执行,因此mounted子>父,因此顺序是函数
父beforeMount > 子beforMount > 子mounted > 父mounted
总体初次渲染的顺序是
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
这两个钩子都是在组件更新的时候触发的,在$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
这两个钩子都是在组件销毁过程当中执行的,在组件更新过程当中,会进行新旧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
这两个钩子的话是应用在keep-alive组件所包裹的组件下的,跟mounted和destroyed钩子相似,再代码判断里,经过vnode.data.keepLive来区分普通非缓存组件,进而执行不一样的钩子
这篇是总结了vue10个生命周期运行机制,若是你有幸看完了,若是有什么不对的地方,请评论指出或私自探讨一下,若是以为不错,点个赞吧。哈哈