本文主要来剖析 Vue3.0 编译阶段做的优化,在 patch 阶段是怎么利用这些优化战略来削减比对次数。 因为组件更新时依然需要遍历该组件的整个 vnode 树,比方下面这个模板:

<template>
  <div id="container">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{ message }}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</template>

整个 diff 进程如图所示:

Vue3编译优化
能够看到,因为这段代码中只要一个动态节点,所以这里有许多 diff 和遍历其实都是不需要的,这就会导致 vnode 的功能跟模版巨细正相关,跟动态节点的数量无关,当一些组件的整个模版内只要少量动态节点时,这些遍历都是功能的糟蹋。对于上述比方,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。

Vue.js 3.0 经过编译阶段对静态模板的剖析,编译生成了 Block tree

Block tree 是一个将模板依据动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。凭借 Block treeVue.js 将 vnode 更新功能由与模版全体巨细相关提高为与动态内容的数量相关,这是一个非常大的功能突破。

PatchFlag

因为 diff 算法无法防止新旧虚拟 DOM 中无用的比较操作,Vue.js 3.0 引入了 patchFlag,用来标记动态内容。在编译进程中会依据不同的特点类型打上不同的标识,从而完成了快速 diff 算法。PatchFlags 的所有枚举类型如下所示:

export const enum PatchFlags {
  TEXT = 1, // 动态文本节点
  CLASS = 1 << 1, // 动态class
  STYLE = 1 << 2, // 动态style
  PROPS = 1 << 3, // 除了class、style动态特点
  FULL_PROPS = 1 << 4, // 有key,需要完整diff
  HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
  STABLE_FRAGMENT = 1 << 6, // 安稳序列,子节点顺序不会发生改变
  KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
  UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
  NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
  DYNAMIC_SLOTS = 1 << 10, // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, 
  HOISTED = -1, // 表示静态节点,内容改变,不比较儿子
  BAIL = -2 // 表示diff算法应该结束
}

Block Tree

Vue3编译优化
左边的 template 经过编译后会生成右侧的 render 函数,里边有 _openBlock_createElementBlock_toDisplayString_createElementVNode(createVnode) 等辅佐函数。

let currentBlock = null
function _openBlock() {
  currentBlock = [] // 用一个数组来搜集多个动态节点
}
function _createElementBlock(type, props, children, patchFlag) {
  return setupBlock(createVnode(type, props, children, patchFlag));
}
export function createVnode(type, props, children = null, patchFlag = 0) {
  const vnode = {
    type,
    props,
    children,
    el: null, // 虚拟节点上对应的实在节点,后续diff算法
    key: props?.["key"],
    __v_isVnode: true,
    shapeFlag,
    patchFlag 
  };
  ...
  if (currentBlock && vnode.patchFlag > 0) {
    currentBlock.push(vnode);
  }
  return vnode;
}
function setupBlock(vnode) {
  vnode.dynamicChildren = currentBlock;
  currentBlock = null;
  return vnode;
}
function _toDisplayString(val) {
  return isString(val)
    ? val
    : val == null
    ? ""
    : isObject(val)
    ? JSON.stringify(val)
    : String(val);
}

此刻生成的 vnode 如下:

Vue3编译优化
此刻生成的虚拟节点多出一个 dynamicChildren 特点,里边搜集了动态节点 span

节点 diff 优化战略:

咱们之前剖析过,在 patch 阶段更新节点元素的时分,会履行 patchElement 函数,咱们再来回顾一下它的完成:

const patchElement = (n1, n2) => { // 先复用节点、在比较特点、在比较儿子
  let el = n2.el = n1.el;
  let oldProps = n1.props || {}; // 目标
  let newProps = n2.props || {}; // 目标
  patchProps(oldProps, newProps, el);
  if (n2.dynamicChildren) { // 只比较动态元素
    patchBlockChildren(n1, n2);
  } else {
    patchChildren(n1, n2, el); // 全量 diff
  }
}

咱们在前面组件更新的章节剖析过这个流程,在剖析子节点更新的部分,其时并没有考虑到优化的场景,所以只剖析了全量比对更新的场景。

而实际上,假如这个 vnode 是一个 Block vnode,那么咱们不用去经过 patchChildren 全量比对,只需要经过 patchBlockChildren 去比对并更新 Block 中的动态子节点即可。 由此能够看出功能被大幅度提高,从 tree 等级的比对,变成了线性结构比对。

咱们来看一下它的完成:

const patchBlockChildren = (n1, n2) => {
  for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
  }
}

特点 diff 优化战略:

接下来咱们看一下特点比对的优化战略:

const patchElement = (n1, n2) => { // 先复用节点、在比较特点、在比较儿子
  let el = n2.el = n1.el;
  let oldProps = n1.props || {}; // 目标
  let newProps = n2.props || {}; // 目标
  let { patchFlag, dynamicChildren } = n2
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) { // 对所 props 都进行比较更新
      patchProps(el, n2, oldProps, newProps, ...)
    } else {
      // 存在动态 class 特点时
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, 'class', null, newProps.class, ...)
        }
      }
      // 存在动态 style 特点时
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, 'style', oldProps.style, newProps.style, ...)
      }
      // 针对除了 style、class 的 props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          if (next !== prev) {
            hostPatchProp(el, key, prev, next, ...)
          }
        }
      }
      if (patchFlag & PatchFlags.TEXT) { // 存在动态文本
        if (n1.children !== n2.children) {
          hostSetElementText(el, n2.children as string)
        }
      } 
    } else if (dynamicChildren == null) {
      patchProps(el, n2, oldProps, newProps, ...)
    }
  }
}
function hostPatchProp(el, key, prevValue, nextValue) {
  if (key === 'class') { // 更新 class 
    patchClass(el, nextValue)
  } else if (key === 'style') { // 更新 style
    patchStyle(el, prevValue, nextValue)
  } else if (/^on[^a-z]/.test(key)) {  // events  addEventListener
    patchEvent(el, key, nextValue);
  } else { // 一般特点 el.setAttribute
    patchAttr(el, key, nextValue);
  }
}
function patchClass(el, nextValue) {
  if (nextValue == null) {
    el.removeAttribute('class'); // 假如不需要class直接移除
  } else {
    el.className = nextValue
  }
}
function patchStyle(el, prevValue, nextValue = {}){
  ...
}
function patchAttr(el, key, nextValue){
  ...
}

总结: vue3 会充分利用 patchFlagdynamicChildren 做优化。假如确认只是某个局部的变动,比方 style 改变,那么只会调用 hostPatchProp 并传入对应的参数 style 做特定的更新(靶向更新);假如有 dynamicChildren,会履行 patchBlockChildren 做比照更新,不会每次都对 props 和子节点进行全量的比照更新。图解如下:

Vue3编译优化

静态提高

静态提高是将静态的节点或者特点提高出去,假设有以下模板:

<div>
  <span>hello</span> 
  <span a=1 b=2>{{name}}</span>
  <a><span>{{age}}</span></a>
</div>

编译生成的 render 函数如下:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "hello"),
    _createElementVNode("span", {
      a: "1",
      b: "2"
    }, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}

咱们把模板编译成 render 函数是这个酱紫的,那么问题便是每次调用 render 函数都要重新创立虚拟节点。

敞开静态提高 hoistStatic 选项后

const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
  a: "1",
  b: "2"
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}

预解析字符串化

静态提高的节点都是静态的,咱们能够将提高出来的节点字符串化。 当接连静态节点超越 10 个时,会将静态节点序列化为字符串。

假如有如下模板:

<div>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
</div>

敞开静态提高 hoistStatic 选项后

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span>", 10)
const _hoisted_11 = [  _hoisted_1]
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
}

函数缓存

假如有如下模板:

<div @click="event => v = event.target.value"></div>

编译后:

const _hoisted_1 = ["onClick"]
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: event => _ctx.v = event.target.value
  }, null, 8 /* PROPS */, _hoisted_1))
}

每次调用 render 的时分要创立新函数,敞开函数缓存 cacheHandlers 选项后,函数会被缓存起来,后续能够直接运用

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: _cache[0] || (_cache[0] = event => _ctx.v = event.target.value)
  }))
}

总结

以上几点即为 Vuejs 在编译阶段做的优化,依据上面几点,Vuejspatch 进程中极大地提高了功能。