根據調試工具看Vue源碼之虛擬dom(三)

前言

上回我們瞭解了 vnode 從創建到生成的流程,這回我們來探索 Vue 是如何將 vnode 轉化成真實的 dom 節點/元素

Vue.prototype._update

上次我們提到的 _render 函數其實作爲 _update 函數的參數傳入,換句話說,_render 函數結束後 _update 將會執行👇

Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var restoreActiveInstance = setActiveInstance(vm);
    vm._vnode = vnode;
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    restoreActiveInstance();
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el;
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  };

簡單梳理下這段代碼的邏輯:

  • 調用 setActiveInstance(vm) 設置當前的 vm 爲活躍的實例
  • 判斷 preVnode 是否存在,是則調用 vm.$el = vm.__patch__(prevVnode, vnode);,否則調用 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);(其實也就是第一次渲染跟二次更新的區別)
  • 調用 restoreActiveInstance() 重置活躍的實例
  • HOC 做了特殊判斷(因爲沒用過 HOC,所以這裏直接略過)

從上面整理下來的邏輯中,我們能得到訊息僅僅只有 setActiveInstance 函數返回一個閉包函數(當然這並不是很重要),如果需要更深入的瞭解,還需要了解 __patch__ 函數是怎麼實現的

其他相關代碼:

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};
...
new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);

__patch__

說出來你可能不信,__patch__ 函數的實現其實很簡單👇

var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
...
Vue.prototype.__patch__ = inBrowser ? patch : noop;

很明顯,createPatchFunction 也是返回了一個閉包函數

patch

雖然 __patch__ 外表看起來很簡單,但是其實內部實現的邏輯還是挺複雜的,代碼量也非常多👇

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }

    var isInitialPatch = false;
    var insertedVnodeQueue = [];

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR);
            hydrating = true;
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true);
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              );
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode);
        }

        // replacing existing element
        var oldElm = oldVnode.elm;
        var parentElm = nodeOps.parentNode(oldElm);

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        );

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          var ancestor = vnode.parent;
          var patchable = isPatchable(vnode);
          while (ancestor) {
            for (var i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor);
            }
            ancestor.elm = vnode.elm;
            if (patchable) {
              for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
                cbs.create[i$1](emptyNode, ancestor);
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              var insert = ancestor.data.hook.insert;
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
                  insert.fns[i$2]();
                }
              }
            } else {
              registerRef(ancestor);
            }
            ancestor = ancestor.parent;
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0);
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }

這麼多的代碼,一下子肯定是消化不完的,所以我們可以嘗試性的帶着以下這幾個問題來看👇

  • 第一次的 patch 操作與後續的 patch 操作有何區別?
  • dom 節點之間產生變更,或者說是「新節點」替換「老節點」時,規則是怎麼樣的?

patch 函數的特殊邏輯

針對初次渲染,patch 函數是做了特殊邏輯的。顯然我們只要把初次執行的 patch 的邏輯走一遍就清楚了👇

結合上面的源碼,歸納下這裏的思路:

  • 若「老節點」爲空,則調用 createElm(vnode, insertVnodeQueue)來 直接創建「新節點」
  • 若「老節點」爲真實存在的 dom 節點,則分成以下幾步:

    • 移除 「老節點」的 SSR_ATTR 屬性(若存在)
    • 判斷是否正在「渲染」(hydrating

      • 是則執行hydrate(oldvnode, vnode, insertVnodeQueue)並判斷是否執行成功

        • 成功後觸發 invokeInsertHook(vnode, insertVnodeQueue, true)
        • 失敗後發出「警告」(測試環境)
      • 否則調用 emptyNodeAt(oldVnode),給「老節點」(實際上是 dom 節點)生成它的 "vnode"

被「遺忘」的一行代碼

看完源碼的同學不難不發現,上面梳理的邏輯裏少了這段代碼:

if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // patch existing root node
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}

也就是對「非 dom 元素的相同節點」做一次 patchVnode 的操作。關於這段代碼可以分成幾點來分析:

  • 什麼是「相同節點」?
  • patchVnode 做了什麼?

「相同的節點」

根據語義我們應該看這部分代碼👇

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

sameVnode 的邏輯就是:按照 vnode 的屬性來判斷兩個 「vnode」節點是否是同一個節點

patchVnode

由於執行 patchVnode 的前提就是新老節點是「相同」的節點,我們有理由相信,它是用來處理同個節點的變化。

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode);
    }

    var elm = vnode.elm = oldVnode.elm;

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
      } else {
        vnode.isAsyncPlaceholder = true;
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance;
      return
    }

    var i;
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode);
    }

    var oldCh = oldVnode.children;
    var ch = vnode.children;
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch);
        }
        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text);
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
    }
  }

我們看看這段代碼都做了哪些事情:

  1. 複用 vnode(如果存在 elem 屬性)
  2. 處理異步組件
  3. 處理靜態節點
  4. 執行 prepatch(如果存在 data 屬性)
  5. 執行 update(如果存在 data 屬性)
  6. 比較 oldVnodevnode 兩個節點
  7. 執行 postpatch(如果存在 data 屬性)

當然,這裏最直觀的就是比較 oldVnodevnode 兩個節點的邏輯👇
3.png

其他的邏輯可以留到下一篇文章再分析~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章