Vue3中對VDOM的改進

前言

vue-next 對virtual dom的patch更新作了一系列的優化,從編譯時加入了 block 以減小 vdom 之間的對比次數,另外還有 hoisted 的操做減小了內存的開銷。本文寫給本身看,作個知識點記錄,若有錯誤,還請不吝賜教。html

VDOM

VDOM的概念簡單來講就是用js對象來模擬真實DOM樹。因爲MV**的架構,真實DOM樹應該隨着數據(Vue2.x中的data)的改變而發生改變,這些改變多是如下幾個方面:vue

  • v-if
  • v-for
  • 動態的props(如:class,@click)
  • 子節點的改變
  • 等等

Vue框架要作的其實很單一:在用戶改變數據時,正確更新DOM樹,作法就是其核心的VDOM的patch和diff算法。node

Vue2.x中的作法

在Vue2.x中,當數據改變後就要對全部的節點進行patch和diff操做。如如下DOM結構:算法

<div>
  <span class="header">I'm header</span>
  <ul>
    <li>第一個靜態li</li>
    <li v-for="item in mutableItems" :key="item.key"> {{ item.desc }}</li>
  </ul>
</div>

在第一次mount節點的時候會去生成真實的DOM,此後若是數組

mutableItems.push({
  key: 'asdf',
  desc: 'a new li item'
})

預期的結果是頁面出現新的一個li元素,內容就是 a new li item,Vue2.x中是經過patch時對 ul 元素對應的 vnodechildren 來進行 diff 操做,具體操做在此不深究,可是該操做是須要比較全部的 li 對應的 vnode 的。架構

不足

正是因爲2.x版本中的diff操做須要遍歷全部元素,本例中包括了 span 和 第一個li元素,可是這兩個元素是靜態的,不須要被比較的,不論數據怎麼變,靜態元素都不會再更改了。vue-next在編譯時對這種操做作了優化,即 Block框架

Block

入上述模板,在vue-next中生成的渲染函數爲:dom

const _Vue = Vue
const { createVNode: _createVNode } = _Vue

const _hoisted_1 = _createVNode("span", { class: "header" }, "I'm header", -1 /* HOISTED */)
const _hoisted_2 = _createVNode("li", null, "第一個靜態li", -1 /* HOISTED */)

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode: _createVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _hoisted_1,
      _createVNode("ul", null, [
        _hoisted_2,
        (_openBlock(true), _createBlock(_Fragment, null, _renderList(state.mutableItems, (item) => {
          return (_openBlock(), _createBlock("li", { key: item.key }, _toDisplayString(item.desc), 1 /* TEXT */))
        }), 128 /* KEYED_FRAGMENT */))
      ])
    ], 64 /* STABLE_FRAGMENT */))
  }
}

咱們能夠看到調用了 openBlockcreateBlock 方法,這兩個方法的代碼實現也很簡單:ide

const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
let shouldTrack = 1
// openBlock
export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}
export function createBlock(
  type: VNodeTypes | ClassComponent,
  props?: { [key: string]: any } | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[]
): VNode {
  // avoid a block with patchFlag tracking itself
  shouldTrack--
  const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
  shouldTrack++
  // save current block children on the block vnode
  vnode.dynamicChildren = currentBlock || EMPTY_ARR
  // close block
  blockStack.pop()
  currentBlock = blockStack[blockStack.length - 1] || null
  // a block is always going to be patched, so track it as a child of its
  // parent block
  if (currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}

更加詳細的註釋還請看源代碼中的註釋,寫的十分詳盡,便於理解。這裏面 openBlock 就是初始化一個塊,createBlock 就是對當前編譯的內容生成一個塊,這裏面的這一行代碼:vnode.dynamicChildren = currentBlock || EMPTY_ARR 就是在收集動態的子節點,咱們能夠再看一下編譯時運行的函數:函數

// createVNode
function _createVNode(
  type: VNodeTypes | ClassComponent,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null
) {
  /**
   *  一系列代碼
  **/

  // presence of a patch flag indicates this node needs patching on updates.
  // component nodes also should always be patched, because even if the
  // component doesn't need to update, it needs to persist the instance on to
  // the next vnode so that it can be properly unmounted later.
  if (
    shouldTrack > 0 &&
    currentBlock &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    patchFlag !== PatchFlags.HYDRATE_EVENTS &&
    (patchFlag > 0 ||
      shapeFlag & ShapeFlags.SUSPENSE ||
      shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
      shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
  ) {
    currentBlock.push(vnode)
  }
}

上述函數是在模板編譯成ast以後調用的生成VNode的函數,因此有patchFlag這個標誌,若是是動態的節點,而且此時是開啓了Block的話,就會將節點塞入Block中,這樣 createBlock返回的 VNode 中就會有 dynamicChildren 了。

到此爲止,經過本文中案例通過模板編譯和render函數運行後並通過了優化之後生成了以下結構的vnode:

const result = {
  type: Symbol(Fragment),
  patchFlag: 64,
  children: [
    { type: 'span', patchFlag: -1, ...},
    {
      type: 'ul',
      patchFlag: 0,
      children: [
        { type: 'li', patchFlag: -1, ...},
        {
          type: Symbol(Fragment),
          children: [
            { type: 'li', patchFlag: 1 ...},
            { type: 'li', patchFlag: 1 ...}
          ]
        }
      ]
    }
  ],
  dynamicChildren: [
    {
      type: Symbol(Fragment),
      patchFlag: 128,
      children: [
        { type: 'li', patchFlag: 1 ...},
        { type: 'li', patchFlag: 1 ...}
      ]
    }
  ]
}

以上的 result 不完整,可是咱們暫時只關心這些屬性。能夠看見 result.children 的第一個元素是span,patchFlag=-1,且 result 有一個 dynamicChildren 數組,裏面只包含了兩個動態的 li,後續若是變更了數據,那麼新的 vnode.dynamicChildren 會有第三個 li 元素。

patch

patch部分其實也沒差多少,就是根據vnode的type執行不一樣的patch操做:

function patchElement(n1, n2) {
  let { dynamicChildren } = n2
  // 一系列操做

  if (dynamicChildren) {
    patchBlockChildren (
      n1.dynamicChildren!,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG
    )
  } else if (!optimized) {
    // full diff
    patchChildren(
      n1,
      n2,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG
    )
  }
}

能夠看見,若是有了 dynamicChildren 那麼vue2.x版本中的diff操做就被替換成了 patchBlockChildren() 且參數只有 dynamicChildren,就是靜態的不作diff操做了,而若是vue-next的patch中沒有 dynamicChildren,則進行完整的diff操做,入註釋寫的 full diff 的後續代碼。

結尾

本文沒有深刻講解代碼的實現層面,一是由於本身實力不濟還在閱讀源碼當中,二是我我的認爲閱讀源碼不可鑽牛角尖,從大局入眼,再徐徐圖之,先明白了各個部分的做用後帶着思考去閱讀源碼能收穫到的應該更多一些。

諸君共勉。

相關文章
相關標籤/搜索