刁鑽面試官:vue 節點銷燬的時候做了些什麼?

kun坤發表於2020-04-07

面試路上難免磕磕絆絆
但是沒想到這次遇到狼滅了

"你知道 vue 節點銷燬的時候做了些什麼嗎?"
"..."

vue生命週期

我們知道vue的生命週期有這些

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
];
複製程式碼

其中'beforeDestroy', 'destroyed',是我們自己可以編寫的生命週期函式
但是除此之外,其實vue還有一些鉤子函式是內部使用的,其中也有destroy鉤子

var hooks = ['create', 'activate', 'update', 'remove', 'destroy'];
複製程式碼

那麼當一個節點銷燬的時候,到底做了些啥呢,首先讓我們來回顧一下vue中一個節點是怎麼被觸發銷燬的

節點如何觸發銷燬?

當我們修改一個data的時候
由劫持的 set 觸發了資料 deps 內所有關聯的 watcher 的更新
接著觸發元件的 update 從而進行新舊節點的 patch 進行相應的 dom 更新

當然 patch 函式的實現很複雜,有疑惑的同學可以去找找資料看看
今天我們這裡只要知道在 patch 函式有三個地方會觸發元件銷燬就行了

1.節點刪除或者替換

這個很好理解,比如我們使用 v-if 來控制一個元件是否存再
舊節點不存在了,這個當然得銷燬掉

或者元件不是同一個 component 了,最常用的例子是 <router-view>,<component :is=""> 等等
這些元件會動態變換自己的元件型別,新的元件會建立出來,於是舊的就需要銷燬了

2.刪除children

接著 patch 進入第二階段,patchVNode,這裡會比對新舊節點是否都有 children
如果新節點沒有子節點,說明是刪除節點操作,則需要銷燬 oldNode 的 children

3.children 內子節點被刪除或者替換

最後是進入 updateChildren 階段,也就是我們常說的 diff 階段
diff 階段會從新舊子節點的首尾進行一個迴圈
分別進首位,尾位,首尾,尾首判斷
進行 patch ,新增,左右移動操作
當迴圈完成後,如果新子節點已經迴圈完了,舊子節點還沒迴圈完
說明需要刪除多餘的節點

那麼銷燬的方法做了些什麼呢?我們來看看原始碼

removeVnode函式

可以看到上邊刪除操作是呼叫的這個方法

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);
        }
      }
    }
  }
複製程式碼

很簡單得亞子,其實就是做了兩個事情
執行平臺的刪除節點方法,然後執行remove鉤子和destroy鉤子

function removeAndInvokeRemoveHook (vnode, rm) {
    if (isDef(rm) || isDef(vnode.data)) {
      var i;
      var listeners = cbs.remove.length + 1;
      if (isDef(rm)) {
        // we have a recursively passed down rm callback
        // increase the listeners count
        rm.listeners += listeners;
      } else {
        // directly removing
        rm = createRmCb(vnode.elm, listeners);
      }
      // recursively invoke hooks on child component root node
      if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
        removeAndInvokeRemoveHook(i, rm);
      }
      for (i = 0; i < cbs.remove.length; ++i) {
        cbs.remove[i](vnode, rm);
      }
      if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
        i(vnode, rm);
      } else {
        rm();
      }
    } else {
      removeNode(vnode.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]);
      }
    }
  }
複製程式碼

進而可以發現,實際上兩個方法都很類似
都是遞迴呼叫自己和子節點的remove鉤子和destroy鉤子
其中remove稍有特殊,執行完鉤子函式之後,還會執行真正的平臺移除節點方法

removeNode(childElm);
複製程式碼

remove & destroy 鉤子

於是現在只要知道鉤子函式內執行了什麼就ok了
我們找到定義所有鉤子函式的地方,找到所有鉤子函式

var baseModules = [
  ref,
  directives
]

var platformModules = [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]
複製程式碼

這就是vue定義的 基礎module 和 平臺module
但並不是所有 module 都需要在 remove 階段 和destroy 階段執行鉤子函式

我們進一步檢視程式碼,會發現只有
ref,directives,transition 定義了remove或者destroy鉤子

ref

其中ref是更新我們定義的ref引用資訊 destroy 鉤子是清除真實dom的引用,程式碼如下

var ref = {
  create: function create (_, vnode) {
    ...
  },
  update: function update (oldVnode, vnode) {
    ...
  },
  destroy: function destroy (vnode) {
    registerRef(vnode, true);
  }
}

function registerRef (vnode, isRemoval) {
  ...
  if (isRemoval) {
    if (Array.isArray(refs[key])) {
      remove(refs[key], ref);
    } else if (refs[key] === ref) {
      refs[key] = undefined;
    }
  } else {
    ...
  }
}

複製程式碼

directives

directives 則是更新我們的自定義指令,從而觸發指令的 unbind 事件


function updateDirectives (oldVnode, vnode) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode);
  }
}

function _update (oldVnode, vnode) {
  var isCreate = oldVnode === emptyNode;
  ...
  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // no longer present, unbind
        callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
      }
    }
  }
}
複製程式碼

transition

再來看看transition,它只有remove鉤子 實際上就是就是執行

var transition = inBrowser ? {
  create: _enter,
  activate: _enter,
  remove: function remove$$1 (vnode, rm) {
    /* istanbul ignore else */
    if (vnode.data.show !== true) {
      leave(vnode, rm);
    } else {
      rm();
    }
  }
} : {}

複製程式碼

這裡可以看到銷燬的時候,會觸發transition的leave方法
其中就會執行對應的離開動畫

leave方法在 show 指令的 update 方法中同樣會執行,可以解釋為什麼v-show也能觸發transition的動畫

vue元件的destroy鉤子

除了modules的鉤子,當然vue元件自己的鉤子也是很重要的
在 createComponent 建立元件的時候,會執行installComponentHooks(data);
這裡就會將鉤子函式繫結到data.hook上

然後節點銷燬的時候,如果有這個鉤子就會執行

 if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
複製程式碼

那麼我們只要看看destroy裡做了什麼就行了

var componentVNodeHooks = {
  init: function init (
    vnode,
    hydrating,
    parentElm,
    refElm
  ) {
    ...
  },

  prepatch: function prepatch (oldVnode, vnode) {
    ...
  },

  insert: function insert (vnode) {
    ...
  },

  destroy: function destroy (vnode) {
    var componentInstance = vnode.componentInstance;
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent(componentInstance, true /* direct */);
      }
    }
  }
};
複製程式碼

這裡可以看到,元件銷燬的時候
就是執行了元件的$destroy函式

當然,如果元件是被keepAlive快取的,就不會銷燬,只會進入deactive流程

其中銷燬做了這些操作

Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
    // remove self from parent
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--;
    }
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, 'destroyed');
    // turn off all instance listeners.
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null;
    }
  };
}
複製程式碼

這裡首先觸發我們自己編寫的beforeDestroy生命週期函式
然後清除vnode子節點,接著清除watchers所有對應的依賴
接下來這一段很有意思

vm.__patch__(vm._vnode, null);
複製程式碼
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    ...
}
複製程式碼

這裡其實就是針對元件的遞迴呼叫銷燬,然後觸發destroyed生命週期函式
所以我們元件的銷燬時間執行的順序為:

父元件beforeDestroy -> 子元件beforeDestroy -> 子元件destroy ->父元件destroy

再下來執行$off函式
將_events置空

最後再清除相關節點的引用就結束了

總結

所以,總結一下其實vue銷燬一個節點還是做了不少操作的
會依次執行ref director transition 的remove或者destroy鉤子
然後執行vue元件銷燬方法

over~

相關文章