Vue.js 源碼學習六 —— VNode虛擬DOM學習

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

所謂虛擬DOM,是一個用於表示真實 DOM 結構和屬性的 JavaScript 對象,這個對象用於對比虛擬 DOM 和當前真實 DOM 的差別化,而後進行局部渲染從而實現性能上的優化。在Vue.js 中虛擬 DOM 的 JavaScript 對象就是 VNode。
接下來咱們一步步分析:html

VNode 是什麼?


既然是虛擬 DOM 的做用是轉爲真實的 DOM,那這就是一個渲染的過程。因此咱們看看 render 方法。在以前的學習中咱們知道了,vue 的渲染函數 _render 方法返回的就是一個 VNode 對象。而在 initRender 初始化渲染的方法中定義的 vm._cvm.$createElement 方法中,createElement 最終也是返回 VNode 對象。因此 VNode 是渲染的關鍵所在。
話很少說,來看看這個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 // 當前節點標籤名
    this.data = data // 當前節點數據(VNodeData類型)
    this.children = children // 當前節點子節點
    this.text = text // 當前節點文本
    this.elm = elm // 當前節點對應的真實DOM節點
    this.ns = undefined // 當前節點命名空間
    this.context = context // 當前節點上下文
    this.fnContext = undefined // 函數化組件上下文
    this.fnOptions = undefined // 函數化組件配置項
    this.fnScopeId = undefined // 函數化組件ScopeId
    this.key = data && data.key // 子節點key屬性
    this.componentOptions = componentOptions // 組件配置項 
    this.componentInstance = undefined // 組件實例
    this.parent = undefined // 當前節點父節點
    this.raw = false // 是否爲原生HTML或只是普通文本
    this.isStatic = false // 靜態節點標誌 keep-alive
    this.isRootInsert = true // 是否做爲根節點插入
    this.isComment = false // 是否爲註釋節點
    this.isCloned = false // 是否爲克隆節點
    this.isOnce = false // 是否爲v-once節點
    this.asyncFactory = asyncFactory // 異步工廠方法 
    this.asyncMeta = undefined // 異步Meta
    this.isAsyncPlaceholder = false // 是否爲異步佔位
  }

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

其實就是一個普通的 JavaScript Class 類,中間有各類數據用於描述虛擬 DOM,下面用一個例子來看看VNode 是如何表現 DOM 的。vue

<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>

這個例子最終結果如圖:HTML顯示結果
簡化後的VNode對象結果如圖:node

{
    "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的時候小結如下幾點:git

  • 全部對象的 context 選項都指向了 Vue 實例。
  • elm 屬性則指向了其相對應的真實 DOM 節點。
  • DOM 中的文本內容被當作了一個只有 text 沒有 tag 的節點。
  • 像 class、id 等HTML屬性都放在了 data

咱們瞭解了VNode 是如何描述 DOM 以後,來學習如何將虛擬
DOM 變爲真實的 DOM。github

patch —— Virtual DOM 的核心


從以前的文章中能夠知道,Vue的渲染過程(不管是初始化視圖仍是更新視圖)最終都將走到 _update 方法中,再來看看這個 _update 方法。web

// 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.
  }

不難發現更新試圖都是使用了 vm.__patch__ 方法,咱們繼續往下跟。json

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

這裏囉嗦一句,要找vue的全局方法,如 vm.aaa ,直接查找 Vue.prototype.aaa 便可。
繼續找下去:數組

// 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) {
    // 當前 VNode 未定義、老的 VNode 定義了,調用銷燬鉤子。
    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 {
      // 當前 VNode 和老 VNode 都定義了,執行更新操做
      // DOM 的 nodeType http://www.w3school.com.cn/jsref/prop_node_nodetype.asp
      const isRealElement = isDef(oldVnode.nodeType) // 是否爲真實 DOM 元素
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 修改已有根節點
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        // 已有真實 DOM 元素,處理 oldVnode
        if (isRealElement) {
          // 掛載一個真實元素,確認是否爲服務器渲染環境或者是否能夠執行成功的合併到真實 DOM 中
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              // 調用 insert 鉤子
              // inserted:被綁定元素插入父節點時調用 
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            }
          }
          // 不是服務器渲染或者合併到真實 DOM 失敗,建立一個空節點替換原有節點
          oldVnode = emptyNodeAt(oldVnode)
        }

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

        // 建立新節點
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 遞歸更新父級佔位節點元素,
        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
          }
        }

        // 銷燬舊節點
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 調用 insert 鉤子
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

具體解析看代碼註釋~拋開調用生命週期鉤子和銷燬就節點不談,咱們發現代碼中的關鍵在於 createElmpatchVnode 方法。

createElm

先看 createElm 方法,這個方法建立了真實 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
    // 建立組件
    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)
    }
  }

重點關注代碼中的方法執行。代碼太多,就不貼出來了,簡單說說用途。

  • cloneVNode 用於克隆當前 vnode 對象。
  • createComponent 用於建立組件,在調用了組件初始化鉤子以後,初始化組件,而且從新激活組件。在從新激活組件中使用 insert 方法操做 DOM。
  • nodeOps.createElementNSnodeOps.createElement 方法,實際上是真實 DOM 的方法。
  • setScope 用於爲 scoped CSS 設置做用域 ID 屬性
  • createChildren 用於建立子節點,若是子節點是數組,則遍歷執行 createElm 方法,若是子節點的 text 屬性有數據,則使用 nodeOps.appendChild(...) 在真實 DOM 中插入文本內容。
  • insert 用於將元素插入真實 DOM 中。

因此,這裏的 nodeOps 指的確定就是真實的 DOM 節點了。最終,這些全部的方法都調用了 nodeOps 中的方法來操做 DOM 元素。

這裏順便科普下 DOM 的屬性和方法。下面把源碼中用到的幾個方法列出來便於學習:

  • appendChild: 向元素添加新的子節點,做爲最後一個子節點。
  • insertBefore: 在指定的已有的子節點以前插入新節點。
  • tagName: 返回元素的標籤名。
  • removeChild: 從元素中移除子節點。
  • createElementNS: 建立帶有指定命名空間的元素節點。
  • createElement: 建立元素節點。
  • createComment: 建立註釋節點。
  • createTextNode: 建立文本節點。
  • setAttribute: 把指定屬性設置或更改成指定值。
  • nextSibling: 返回位於相同節點樹層級的下一個節點。
  • parentNode: 返回元素父節點。
  • setTextContent: 獲取文本內容(這個未在w3school中找到,不過應該就是這個意思了)。

OK,知道以上方法就比較好理解了,createElm 方法的最終目的就是建立真實的 DOM 對象。

patchVnode

看過了建立真實 DOM 後,咱們來學習虛擬 DOM 如何實現 DOM 的更新。這纔是虛擬 DOM 的存在乎義 —— 比對並局部更新 DOM 以達到性能優化的目的。
看代碼~

// 補丁 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 爲靜態;新舊 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
    // 執行 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,執行 update 方法
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 執行 data.hook.update 鉤子
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 舊 vnode 的 text 選項爲 undefined
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新舊 vnode 都有 children,且不一樣,執行 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) {
      // 有不一樣文本內容,更新文本內容
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 執行 data.hook.postpatch 鉤子,代表 patch 完畢
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

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

  1. 若是兩個vnode相等,不須要 patch,退出。
  2. 若是是異步佔位,執行 hydrate 方法或者定義 isAsyncPlaceholder 爲 true,而後退出。
  3. 若是兩個vnode都爲靜態,不用更新,因此講之前的 componentInstance 實例傳給當前 vnode,並退出。
  4. 執行 prepatch 鉤子。
  5. 遍歷調用 update 回調,並執行 update 鉤子。
  6. 若是兩個 vnode 都有 children,且 vnode 沒有 text、兩個 vnode 不相等,執行 updateChildren 方法。這是虛擬 DOM 的關鍵。
  7. 若是新 vnode 有 children,而老的沒有,清空文本,並添加 vnode 節點。
  8. 若是老 vnode 有 children,而新的沒喲,清空文本,並移除 vnode 節點。
  9. 若是兩個 vnode 都沒有 children,老 vnode 有 text ,新 vnode 沒有 text ,則清空 DOM 文本內容。
  10. 若是老 vnode 和新 vnode 的 text 不一樣,更新 DOM 元素文本內容。
  11. 調用 postpatch 鉤子。

其中,addVnodes 方法和 removeVnodes 都比較簡單,很好理解。這裏咱們來看看關鍵代碼 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 是一個只用於 <transition-group> 的特殊標籤,
    // 確保移除元素過程當中保持一個正確的相對位置。
    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)) {
        // 結束老 vnode 向左一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新舊開始 vnode 類似,進行pacth。開始 vnode 向右一位
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新舊結束 vnode 類似,進行patch。結束 vnode 向左一位
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 新結束 vnode 和老開始 vnode 類似,進行patch。
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 老開始 vnode 插入到真實 DOM 中,老開始 vnode 向右一位,新結束 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
        // 老結束 vnode 和新開始 vnode 類似,進行 patch。
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 老結束 vnode 插入到真實 DOM 中,老結束 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,說明沒有這個元素,建立新 DOM 元素。
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 獲取 vnode
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 若是生成的 vnode 和新開始 vnode 類似,執行 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 大於老結束 idx,若是是有效數據則添加 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. 看參數,其中 oldCh 和 newCh 即表示了新舊 vnode 數組,兩組數組經過比對的方式來差別化更新 DOM。
  2. 定義了一些變量:開始索引值、結束索引值、開始vnode、結束vnode等等……
  3. 進行循環遍歷,遍歷條件爲 oldStartIdx <= oldEndIdx 和 newStartIdx <= newEndIdx,在遍歷過程當中,oldStartIdx 和 newStartIdx 遞增,oldEndIdx 和 newEndIdx 遞減。當條件不符合跳出遍歷循環。
  4. 若是 oldStartVnode 和 newStartVnode 類似,執行 patch。

image

  1. 若是 oldEndVnode 和 newEndVnode 類似,執行 patch。
  2. 若是 oldStartVnode 和 newEndVnode 類似,執行 patch,而且將該節點移動到 vnode 數組末一位。

image

  1. 若是 oldEndVnode 和 newStartVnode 類似,執行 patch,而且將該節點移動到 vnode 數組第一位。

image

  1. 若是沒有相同的 idx,執行 createElm 方法建立元素。
  2. 若是若有相同的 idx,若是兩個 vnode 類似,執行 patch,而且將該節點移動到 vnode 數組第一位。若是兩個 vnode 不類似,視爲新元素,執行 createElm 建立。

image

  1. 若是老 vnode 數組的開始索引大於結束索引,說明新 node 數組長度大於老 vnode 數組,執行 addVnodes 方法添加這些新 vnode 到 DOM 中。

image

  1. 若是老 vnode 數組的開始索引小於結束索引,說明老 node 數組長度大於新 vnode 數組,執行 removeVnodes 方法從 DOM 中移除老 vnode 數組中多餘的 vnode。

image

嗯……就是這樣!

最後

畢竟是Vue的核心功能之一,雖然省略了很多代碼,但博客篇幅很長。寫了兩天才寫完。不過寫完博客後感受對於 Vue 的理解又加深了不少。
在下一篇博客中,咱們一塊兒來學習template的解析。

參考文檔

Vue.js學習系列

鑑於前端知識碎片化嚴重,我但願可以系統化的整理出一套關於Vue的學習系列博客。

Vue.js學習系列項目地址

本文源碼已收入到GitHub中,以供參考,固然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

關於做者

VioletJack,高效學習前端工程師,喜歡研究提升效率的方法,也專一於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
簡書: http://www.jianshu.com/users/...
Github: https://github.com/violetjack

相關文章
相關標籤/搜索