从源码深刻分析vue生命周期执行机制

前言

最近在工做之余也陆陆续续研究了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>
}

beforeCreate和created钩子

咱们从入口分析起,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.&dollar;vnode为null的状况下,&dollar;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

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个生命周期运行机制,若是你有幸看完了,若是有什么不对的地方,请评论指出或私自探讨一下,若是以为不错,点个赞吧。哈哈

相关文章
相关标签/搜索