從源碼深刻分析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個生命週期運行機制,若是你有幸看完了,若是有什麼不對的地方,請評論指出或私自探討一下,若是以爲不錯,點個贊吧。哈哈

相關文章
相關標籤/搜索