Vue.js 源碼學(xué)習(xí)六 —— VNode虛擬DOM學(xué)習(xí)

初六和家人出去玩,沒寫完博客。跳票了~

所謂虛擬DOM,是一個(gè)用于表示真實(shí) DOM 結(jié)構(gòu)和屬性的 JavaScript 對象,這個(gè)對象用于對比虛擬 DOM 和當(dāng)前真實(shí) DOM 的差異化,然后進(jìn)行局部渲染從而實(shí)現(xiàn)性能上的優(yōu)化。在Vue.js 中虛擬 DOM 的 JavaScript 對象就是 VNode。
接下來我們一步步分析:

VNode 是什么?


既然是虛擬 DOM 的作用是轉(zhuǎn)為真實(shí)的 DOM,那這就是一個(gè)渲染的過程。所以我們看看 render 方法。在之前的學(xué)習(xí)中我們知道了,vue 的渲染函數(shù) _render 方法返回的就是一個(gè) VNode 對象。而在 initRender 初始化渲染的方法中定義的 vm._cvm.$createElement 方法中,createElement 最終也是返回 VNode 對象。所以 VNode 是渲染的關(guān)鍵所在。
話不多說,來看看這個(gè)VNode為何方神圣。

// src/core/vdom/vnode.js
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functioanl scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag // 當(dāng)前節(jié)點(diǎn)標(biāo)簽名
    this.data = data // 當(dāng)前節(jié)點(diǎn)數(shù)據(jù)(VNodeData類型)
    this.children = children // 當(dāng)前節(jié)點(diǎn)子節(jié)點(diǎn)
    this.text = text // 當(dāng)前節(jié)點(diǎn)文本
    this.elm = elm // 當(dāng)前節(jié)點(diǎn)對應(yīng)的真實(shí)DOM節(jié)點(diǎn)
    this.ns = undefined // 當(dāng)前節(jié)點(diǎn)命名空間
    this.context = context // 當(dāng)前節(jié)點(diǎn)上下文
    this.fnContext = undefined // 函數(shù)化組件上下文
    this.fnOptions = undefined // 函數(shù)化組件配置項(xiàng)
    this.fnScopeId = undefined // 函數(shù)化組件ScopeId
    this.key = data && data.key // 子節(jié)點(diǎn)key屬性
    this.componentOptions = componentOptions // 組件配置項(xiàng) 
    this.componentInstance = undefined // 組件實(shí)例
    this.parent = undefined // 當(dāng)前節(jié)點(diǎn)父節(jié)點(diǎn)
    this.raw = false // 是否為原生HTML或只是普通文本
    this.isStatic = false // 靜態(tài)節(jié)點(diǎn)標(biāo)志 keep-alive
    this.isRootInsert = true // 是否作為根節(jié)點(diǎn)插入
    this.isComment = false // 是否為注釋節(jié)點(diǎn)
    this.isCloned = false // 是否為克隆節(jié)點(diǎn)
    this.isOnce = false // 是否為v-once節(jié)點(diǎn)
    this.asyncFactory = asyncFactory // 異步工廠方法 
    this.asyncMeta = undefined // 異步Meta
    this.isAsyncPlaceholder = false // 是否為異步占位
  }

  // 容器實(shí)例向后兼容的別名
  get child (): Component | void {
    return this.componentInstance
  }
}

其實(shí)就是一個(gè)普通的 JavaScript Class 類,中間有各種數(shù)據(jù)用于描述虛擬 DOM,下面用一個(gè)例子來看看VNode 是如何表現(xiàn) DOM 的。

    <div id="app">
        <span>{{ message }}</span>
        <ul>
            <li v-for="item of list" class="item-cls">{{ item }}</li>
        </ul>
    </div>

    <script>
        new Vue({
            el: '#app',
            data: {
                message: 'hello Vue.js',
                list: ['jack', 'rose', 'james']
            }
        })
    </script>

這個(gè)例子最終結(jié)果如圖:
HTML顯示結(jié)果

簡化后的VNode對象結(jié)果如圖:

{
    "tag": "div",
    "data": {
        "attr": { "id": "app" }
    },
    "children": [
        {
            "tag": "span",
            "children": [
                { "text": "hello Vue.js" }
            ]
        },
        {
            "tag": "ul",
            "children": [
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "jack" }
                    ]
                },
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "rose" }
                    ]
                },
                {
                    "tag": "li",
                    "data": { "staticClass": "item-cls" },
                    "children": [
                        { "text": "james" }
                    ]
                }
            ]
        }
    ],
    "context": "$Vue$3",
    "elm": "div#app"
}

在看VNode的時(shí)候小結(jié)以下幾點(diǎn):

  • 所有對象的 context 選項(xiàng)都指向了 Vue 實(shí)例。
  • elm 屬性則指向了其相對應(yīng)的真實(shí) DOM 節(jié)點(diǎn)。
  • DOM 中的文本內(nèi)容被當(dāng)做了一個(gè)只有 text 沒有 tag 的節(jié)點(diǎn)。
  • 像 class、id 等HTML屬性都放在了 data

我們了解了VNode 是如何描述 DOM 之后,來學(xué)習(xí)如何將虛擬
DOM 變?yōu)檎鎸?shí)的 DOM。

patch —— Virtual DOM 的核心


從之前的文章中可以知道,Vue的渲染過程(無論是初始化視圖還是更新視圖)最終都將走到 _update 方法中,再來看看這個(gè) _update 方法。

  // src/core/instance/lifecycle.js
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    
    if (!prevVnode) {
      // 初始化渲染
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      // no need for the ref nodes after initial patch
      // this prevents keeping a detached DOM tree in memory (#5851)
      vm.$options._parentElm = vm.$options._refElm = null
    } else {
      // 更新渲染
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // 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.
  }

不難發(fā)現(xiàn)更新試圖都是使用了 vm.__patch__ 方法,我們繼續(xù)往下跟。

// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

這里啰嗦一句,要找vue的全局方法,如 vm.aaa ,直接查找 Vue.prototype.aaa 即可。
繼續(xù)找下去:

// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

找到 createPatchFunction 方法~

// src/core/vdom/patch.js
export function createPatchFunction (backend) {
  ……
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    // 當(dāng)前 VNode 未定義、老的 VNode 定義了,調(diào)用銷毀鉤子。
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 老的 VNode 未定義,初始化。
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      // 當(dāng)前 VNode 和老 VNode 都定義了,執(zhí)行更新操作
      // DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp
      const isRealElement = isDef(oldVnode.nodeType) // 是否為真實(shí) DOM 元素
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 修改已有根節(jié)點(diǎn)
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        // 已有真實(shí) DOM 元素,處理 oldVnode
        if (isRealElement) {
          // 掛載一個(gè)真實(shí)元素,確認(rèn)是否為服務(wù)器渲染環(huán)境或者是否可以執(zhí)行成功的合并到真實(shí) DOM 中
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              // 調(diào)用 insert 鉤子
              // inserted:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            }
          }
          // 不是服務(wù)器渲染或者合并到真實(shí) DOM 失敗,創(chuàng)建一個(gè)空節(jié)點(diǎn)替換原有節(jié)點(diǎn)
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 替換已有元素
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 創(chuàng)建新節(jié)點(diǎn)
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 遞歸更新父級占位節(jié)點(diǎn)元素,
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 銷毀舊節(jié)點(diǎn)
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 調(diào)用 insert 鉤子
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

具體解析看代碼注釋~拋開調(diào)用生命周期鉤子和銷毀就節(jié)點(diǎn)不談,我們發(fā)現(xiàn)代碼中的關(guān)鍵在于 createElmpatchVnode 方法。

createElm

先看 createElm 方法,這個(gè)方法創(chuàng)建了真實(shí) DOM 元素。

  function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    // 創(chuàng)建組件
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

重點(diǎn)關(guān)注代碼中的方法執(zhí)行。代碼太多,就不貼出來了,簡單說說用途。

  • cloneVNode 用于克隆當(dāng)前 vnode 對象。
  • createComponent 用于創(chuàng)建組件,在調(diào)用了組件初始化鉤子之后,初始化組件,并且重新激活組件。在重新激活組件中使用 insert 方法操作 DOM。
  • nodeOps.createElementNSnodeOps.createElement 方法,其實(shí)是真實(shí) DOM 的方法。
  • setScope 用于為 scoped CSS 設(shè)置作用域 ID 屬性
  • createChildren 用于創(chuàng)建子節(jié)點(diǎn),如果子節(jié)點(diǎn)是數(shù)組,則遍歷執(zhí)行 createElm 方法,如果子節(jié)點(diǎn)的 text 屬性有數(shù)據(jù),則使用 nodeOps.appendChild(...) 在真實(shí) DOM 中插入文本內(nèi)容。
  • insert 用于將元素插入真實(shí) DOM 中。

所以,這里的 nodeOps 指的肯定就是真實(shí)的 DOM 節(jié)點(diǎn)了。最終,這些所有的方法都調(diào)用了 nodeOps 中的方法來操作 DOM 元素。

這里順便科普下 DOM 的屬性和方法。下面把源碼中用到的幾個(gè)方法列出來便于學(xué)習(xí):

  • appendChild: 向元素添加新的子節(jié)點(diǎn),作為最后一個(gè)子節(jié)點(diǎn)。
  • insertBefore: 在指定的已有的子節(jié)點(diǎn)之前插入新節(jié)點(diǎn)。
  • tagName: 返回元素的標(biāo)簽名。
  • removeChild: 從元素中移除子節(jié)點(diǎn)。
  • createElementNS: 創(chuàng)建帶有指定命名空間的元素節(jié)點(diǎn)。
  • createElement: 創(chuàng)建元素節(jié)點(diǎn)。
  • createComment: 創(chuàng)建注釋節(jié)點(diǎn)。
  • createTextNode: 創(chuàng)建文本節(jié)點(diǎn)。
  • setAttribute: 把指定屬性設(shè)置或更改為指定值。
  • nextSibling: 返回位于相同節(jié)點(diǎn)樹層級的下一個(gè)節(jié)點(diǎn)。
  • parentNode: 返回元素父節(jié)點(diǎn)。
  • setTextContent: 獲取文本內(nèi)容(這個(gè)未在w3school中找到,不過應(yīng)該就是這個(gè)意思了)。

OK,知道以上方法就比較好理解了,createElm 方法的最終目的就是創(chuàng)建真實(shí)的 DOM 對象。

patchVnode

看過了創(chuàng)建真實(shí) DOM 后,我們來學(xué)習(xí)虛擬 DOM 如何實(shí)現(xiàn) DOM 的更新。這才是虛擬 DOM 的存在意義 —— 比對并局部更新 DOM 以達(dá)到性能優(yōu)化的目的。
看代碼~

  // 補(bǔ)丁 vnode
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 新舊 vnode 相等
    if (oldVnode === vnode) {
      return
    }

    const elm = vnode.elm = oldVnode.elm
    // 異步占位
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // 如果新舊 vnode 為靜態(tài);新舊 vnode key相同;
    // 新 vnode 是克隆所得;新 vnode 有 v-once 的屬性
    // 則新 vnode 的 componentInstance 用老的 vnode 的。
    // 即 vnode 的 componentInstance 保持不變。
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    // 執(zhí)行 data.hook.prepatch 鉤子。
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      // 遍歷 cbs,執(zhí)行 update 方法
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 執(zhí)行 data.hook.update 鉤子
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 舊 vnode 的 text 選項(xiàng)為 undefined
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新舊 vnode 都有 children,且不同,執(zhí)行 updateChildren 方法。
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 清空文本,添加 vnode
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 移除 vnode
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 如果新舊 vnode 都是 undefined,清空文本
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 有不同文本內(nèi)容,更新文本內(nèi)容
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 執(zhí)行 data.hook.postpatch 鉤子,表明 patch 完畢
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

源碼中添加了一些注釋便于理解,來理一下邏輯。

  1. 如果兩個(gè)vnode相等,不需要 patch,退出。
  2. 如果是異步占位,執(zhí)行 hydrate 方法或者定義 isAsyncPlaceholder 為 true,然后退出。
  3. 如果兩個(gè)vnode都為靜態(tài),不用更新,所以講以前的 componentInstance 實(shí)例傳給當(dāng)前 vnode,并退出。
  4. 執(zhí)行 prepatch 鉤子。
  5. 遍歷調(diào)用 update 回調(diào),并執(zhí)行 update 鉤子。
  6. 如果兩個(gè) vnode 都有 children,且 vnode 沒有 text、兩個(gè) vnode 不相等,執(zhí)行 updateChildren 方法。這是虛擬 DOM 的關(guān)鍵。
  7. 如果新 vnode 有 children,而老的沒有,清空文本,并添加 vnode 節(jié)點(diǎn)。
  8. 如果老 vnode 有 children,而新的沒喲,清空文本,并移除 vnode 節(jié)點(diǎn)。
  9. 如果兩個(gè) vnode 都沒有 children,老 vnode 有 text ,新 vnode 沒有 text ,則清空 DOM 文本內(nèi)容。
  10. 如果老 vnode 和新 vnode 的 text 不同,更新 DOM 元素文本內(nèi)容。
  11. 調(diào)用 postpatch 鉤子。

其中,addVnodes 方法和 removeVnodes 都比較簡單,很好理解。這里我們來看看關(guān)鍵代碼 updateChildren 方法。

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly 是一個(gè)只用于 <transition-group> 的特殊標(biāo)簽,
    // 確保移除元素過程中保持一個(gè)正確的相對位置。
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 開始老 vnode 向右一位
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 結(jié)束老 vnode 向左一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新舊開始 vnode 相似,進(jìn)行pacth。開始 vnode 向右一位
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新舊結(jié)束 vnode 相似,進(jìn)行patch。結(jié)束 vnode 向左一位
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 新結(jié)束 vnode 和老開始 vnode 相似,進(jìn)行patch。
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 老開始 vnode 插入到真實(shí) DOM 中,老開始 vnode 向右一位,新結(jié)束 vnode 向左一位
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老結(jié)束 vnode 和新開始 vnode 相似,進(jìn)行 patch。
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 老結(jié)束 vnode 插入到真實(shí) DOM 中,老結(jié)束 vnode 向左一位,新開始 vnode 向右一位
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 獲取老 Idx 的 key
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 給老 idx 賦值
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          // 如果老 idx 為 undefined,說明沒有這個(gè)元素,創(chuàng)建新 DOM 元素。
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 獲取 vnode
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 如果生成的 vnode 和新開始 vnode 相似,執(zhí)行 patch。
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 賦值 undefined,插入 vnodeToMove 元素
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 相同的key不同的元素,視為新元素
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // 新開始 vnode 向右一位
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 如果老開始 idx 大于老結(jié)束 idx,如果是有效數(shù)據(jù)則添加 vnode 到新 vnode 中。
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 移除 vnode
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

表示已看暈……讓我們慢慢捋一捋……

  1. 看參數(shù),其中 oldCh 和 newCh 即表示了新舊 vnode 數(shù)組,兩組數(shù)組通過比對的方式來差異化更新 DOM。
  2. 定義了一些變量:開始索引值、結(jié)束索引值、開始vnode、結(jié)束vnode等等……
  3. 進(jìn)行循環(huán)遍歷,遍歷條件為 oldStartIdx <= oldEndIdx 和 newStartIdx <= newEndIdx,在遍歷過程中,oldStartIdx 和 newStartIdx 遞增,oldEndIdx 和 newEndIdx 遞減。當(dāng)條件不符合跳出遍歷循環(huán)。
  4. 如果 oldStartVnode 和 newStartVnode 相似,執(zhí)行 patch。


    image
  5. 如果 oldEndVnode 和 newEndVnode 相似,執(zhí)行 patch。
  6. 如果 oldStartVnode 和 newEndVnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組末一位。


    image
  7. 如果 oldEndVnode 和 newStartVnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組第一位。


    image
  8. 如果沒有相同的 idx,執(zhí)行 createElm 方法創(chuàng)建元素。
  9. 如果如有相同的 idx,如果兩個(gè) vnode 相似,執(zhí)行 patch,并且將該節(jié)點(diǎn)移動到 vnode 數(shù)組第一位。如果兩個(gè) vnode 不相似,視為新元素,執(zhí)行 createElm 創(chuàng)建。


    image
  10. 如果老 vnode 數(shù)組的開始索引大于結(jié)束索引,說明新 node 數(shù)組長度大于老 vnode 數(shù)組,執(zhí)行 addVnodes 方法添加這些新 vnode 到 DOM 中。


    image
  11. 如果老 vnode 數(shù)組的開始索引小于結(jié)束索引,說明老 node 數(shù)組長度大于新 vnode 數(shù)組,執(zhí)行 removeVnodes 方法從 DOM 中移除老 vnode 數(shù)組中多余的 vnode。


    image

嗯……就是這樣!

最后

畢竟是Vue的核心功能之一,雖然省略了不少代碼,但博客篇幅很長。寫了兩天才寫完。不過寫完博客后感覺對于 Vue 的理解又加深了很多。
在下一篇博客中,我們一起來學(xué)習(xí)template的解析。

參考文檔

Vue.js學(xué)習(xí)系列

鑒于前端知識碎片化嚴(yán)重,我希望能夠系統(tǒng)化的整理出一套關(guān)于Vue的學(xué)習(xí)系列博客。

Vue.js學(xué)習(xí)系列項(xiàng)目地址

本文源碼已收入到GitHub中,以供參考,當(dāng)然能留下一個(gè)star更好啦-
https://github.com/violetjack/VueStudyDemos

關(guān)于作者

VioletJack,高效學(xué)習(xí)前端工程師,喜歡研究提高效率的方法,也專注于Vue前端相關(guān)知識的學(xué)習(xí)、整理。
歡迎關(guān)注、點(diǎn)贊、評論留言~我將持續(xù)產(chǎn)出Vue相關(guān)優(yōu)質(zhì)內(nèi)容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571d953d39b0570068145cd1
CSDN: http://blog.csdn.net/violetjack0808
簡書: http://www.itdecent.cn/users/54ae4af3a98d/latest_articles
Github: https://github.com/violetjack

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 寫在前面 這篇文章算是對最近寫的一系列Vue.js源碼的文章(https://github.com/answers...
    染陌同學(xué)閱讀 2,251評論 0 14
  • vue升級到2.0之后就加入了Virtual DOM,對于Virtual DOM的概念這里就不做過多的說明了。本文...
    那少婦閱讀 2,242評論 0 3
  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,186評論 0 29
  • 望著窗外的雨景,淅淅瀝瀝的,令人陶醉,回憶往事歷歷在目,在這個(gè)畢業(yè)的時(shí)節(jié),讓人惋惜,令人傷痛。不論是非成敗,不論豪...
    扶光啟玄閱讀 496評論 1 4
  • 昨天決定又續(xù)了一年的英語學(xué)習(xí)了,本來想放棄的,后來跟老公說,老公說想學(xué)就學(xué)嘛,贊助一半錢,這樣就下決心買一年的了,...
    奔騰君閱讀 163評論 0 0

友情鏈接更多精彩內(nèi)容