Vue.js從Virtual DOM映射到真實DOM的過程

文章首發於:github.com/USTB-musion…javascript

寫在前面

Virtual DOM的概念相信你們都不會陌生,Vritual DOM是相對與DOM(文檔對象模型)來講的,MDN上關於DOM的定義:「DOM模型用一個邏輯樹來表示一個文檔,樹的每一個分支的終點都是一個節點(node),每一個節點都包含着對象(objects)。DOM的方法(methods)讓你能夠用特定方式操做這個樹,用這些方法你能夠改變文檔的結構、樣式或者內容」。相對於頻繁地去操做DOM引發的性能問題,Vritual DOM很好地將DOM作了一層映射關係,將原來須要在DOM上的一系列操做,映射到來操做Virtual DOM。vue

「昂貴」的DOM

爲了有更直觀地感覺「昂貴」的DOM,如今將一個簡單的div元素的全部屬性值打印出來:java

let div = document.createElement('div')
let str = ''
for (let key in div) {
	str += key + ' '
}
複製代碼

打印出來的str值爲:node

可見,真正的DOM元素是很是龐大的,由於瀏覽器把DOM設計地很是複雜,因此當咱們頻繁地去更新DOM時,會產生必定的性能問題。能夠想象,用簡單粗暴的方法將整個DOM結構用innerHTML修改到頁面上,這樣進行重繪整個視圖層是至關消耗性能的。那咱們更新DOM時,能不能只更新修改的地方呢?git

VNode

咱們知道,經歷過render function以後會獲得VNode節點,對這張圖不太明白的話能夠看下我寫的這兩篇文章 Vue.js源碼角度:剖析模版和數據渲染成最終的DOM的過程Vue.js的響應式系統原理Vritual DOM其實就是以VNode節點(JavaScript對象)做爲基礎,用對象屬性來描述節點,實際上它是一層對真實DOM的封裝。Vritual DOM上定義了關於真實DOM的一些關鍵的信息,Vritual DOM徹底是用JS去實現,和宿主瀏覽器沒有任何聯繫,此外得益於js的執行速度,將本來須要在真實DOM進行的建立節點,刪除節點,添加節點等一系列複雜的DOM操做所有放到Vritual DOM中進行。這樣相對與用innerHTML粗暴地重繪整個視圖性能將大大提升。將Virtual DOM修改的地方用diff算法來更新只修改地方,這樣就能避免不少無謂的DOM修改,從而提升了性能。

來看一下Vue.js源碼中關於VNode的定義,定義在src/core/vdom/vnode.js中:github

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; // functional 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
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
複製代碼

其中:算法

tag: 當前節點的標籤名segmentfault

data: 當前節點對應的對象,包含了具體的一些數據信息,是一個VNodeData類型,能夠參考VNodeData類型中的數據信息數組

children: 當前節點的子節點,是一個數組瀏覽器

text: 當前節點的文本

elm: 當前虛擬節點對應的真實dom節點

ns: 當前節點的名字空間

context: 當前節點的編譯做用域

functionalContext: 函數化組件做用域

key: 節點的key屬性,被看成節點的標誌,用以優化

componentOptions: 組件的option選項

componentInstance: 當前節點對應的組件的實例

parent: 當前節點的父節點

raw: 簡而言之就是是否爲原生HTML或只是普通文本,innerHTML的時候爲true,textContent的時候爲false

isStatic: 是否爲靜態節點

isRootInsert: 是否做爲跟節點插入

isComment: 是否爲註釋節點

isCloned: 是否爲克隆節點

isOnce: 是否有v-once指令

舉個例子,咱們如今有這樣一個Vritual DOM:

{
    tag: 'div'
    data: {
        class: 'outer'
    },
    children: [
        {
            tag: 'div',
            data: {
                class: 'inner'
            }
            text: 'Virtual DOM'
        }
    ]
}
複製代碼

渲染以後的真實DOM爲:

<div class="outer">
    <span class="inner">Virtual DOM</span>
</div>
複製代碼

建立一個空VNode節點

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
複製代碼

建立一個文本節點

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}
複製代碼

克隆一個VNode節點

export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}
複製代碼

總的來講,VNode 就是一個 JavaScript 對象,用 JavaScript 對象的屬性來描述當前節點的一些狀態,用 VNode 節點的形式來模擬一棵 Virtual DOM 樹。

更新視圖

咱們知道,Vue.js經過數據綁定來更新視圖,其中會調用updateComponent方法,對這一流程不太明白的話能夠看一下上邊提到的兩篇文章。updateComponent方法定義以下:

updateComponent = () => {
 vm._update(vm._render(), hydrating)
}
複製代碼

該方法會調用vm._update方法,該方法接受的第一個參數是剛生成的VNode,定義在src/core/instance/lifecycle.js中:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = 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)
    }
    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.
  }
複製代碼

其中在關鍵的地方加上註釋:

  // 新的vnode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // 若是須要diff的prevVnode不存在,那麼就用新的vnode建立一個真實dom節點
  if (!prevVnode) {
   // initial render
   // 第一個參數爲真實的node節點
   vm.$el = vm.__patch__(
    vm.$el, vnode, hydrating, false /* removeOnly */,
    vm.$options._parentElm,
    vm.$options._refElm
   )
  } else {
   // updates
   // 若是須要diff的prevVnode存在,那麼首先對prevVnode和vnode進行diff,並將須要的更新的dom操做已patch的形式打到prevVnode上,並完成真實dom的更新工做
   vm.$el = vm.__patch__(prevVnode, vnode)
  }
複製代碼

能夠看到,該方法調用了一個核心方法__patch__,這能夠說是整個Virtual DOM最核心的方法,主要完成了新的虛擬DOM節點和舊的虛擬DOM節點的diff過程,通過patch過程以後生成真實的DOM節點並完成視圖的更新工做。

patch

接下來咱們看一下vm.__patch__方法到底發生了什麼,定義在src/core/vdom/patch.js中:

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

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, 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
        const oldElm = oldVnode.elm
        const 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)) {
          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)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } 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
  }
複製代碼

經過源碼咱們能夠發現,當oldVnode(舊的節點)與vnode(新的節點)在sameVnode的時候纔會進行patchVnode,sameVnode這個方法決定是否要對oldvnode和vnode進行diff和patch的過程。也就是新舊VNode節點斷定爲同一節點的時候纔會進行patchVnode這個過程,不然就是建立新的DOM,移除舊的DOM。下面介紹一下sameVnode方法:

sameVnode

sameVnode定義在src/core/vdom/patch.js中:

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

function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
複製代碼

經過代碼能夠看出,只有當新舊兩個VNode的tag、key、isComment都相同,與此同時定義或未定義data的時候,且若是標籤爲input則type必須相同。這時候這新舊兩個VNode則算sameVnode,接着進行進行patchVnode操做。

diff算法

Vue在2.x版本的vdom算法是基於snabbdom算法所作的修改實現的。

如圖所示,diff算法是經過同層的樹節點進行比較而非對樹進行逐層搜索遍歷的方式,因此時間複雜度只有O(n),是一種很是高效的算法。接下來看一下diff算法最重要的環節updateChildren源碼的實現。

updateChildren

updateChildren源碼的定義在src/core/vdom/patch.js中:

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 is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

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

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
複製代碼

這一塊的源碼解析能夠參考snabbdom源碼學習

相關文章
相關標籤/搜索