你了解 Vue 静态节点的优化吗?

3,574 阅读4分钟

Vuetemplate -> AST -> parse -> optimize -> generate -> render -> vnode 这个template 转化为 vnode 流程中,在 optimize 阶段会做一些优化,就是检测不需要进行DOM改变的静态子树,并做相应处理。

静态节点的定义

那么,Vue 是怎么定义静态节点的呢?来看一下 Vue 源码中是怎么定义的:

// vue/compiler/optimizer.js
// node.type === 1 是标签元素
function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

根据注释,我们可以知道静态节点是:

  1. 节点类型是 text
  2. 预格式化文本节点;
  3. 其他节点,符合以下特征:
    1. 没有动态绑定的
    2. 不包含 v-if , v-for, v-else 指令
    3. 不是构造中的节点(slot, component)
    4. 不是组件类型
    5. 静态节点的父节点,不是带有 v-for 指令的 template 节点
    6. 节点的属性是静态属性

对于第二种类型的静态节点的特征中,第3,4,5,6点的详细解释可能需要结合源码看一下。下面来分析一下。

按顺序来,先看第3点:

isBuiltInTag 定义如下:

// /vue/shared/util.js
// 检查字段是否存在 map 内
export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}
/**
 * Check if a tag is a built-in tag.
 */
export const isBuiltInTag = makeMap('slot,component', true)

从代码中可以看到,buildIn 的节点,指的就是 slotcomponent

下面来看第4点:

isPlatformReservedTag 定义如下

// vue/compiler/optimizer.js
let isPlatformReservedTag
// ...
isPlatformReservedTag = options.isReservedTag || no

从代码中,可以知道,该字段是标记节点标签是否是平台保留的标签,划重点,平台 。也就是说,面对不同平台,该字段执行的方法,匹配的结果也各不相同。

这是 web 平台isReservedTag 方法的定义:

// vue/platforms/web/util/element.js
export const isReservedTag = (tag: string): ?boolean => {
  return isHTMLTag(tag) || isSVG(tag)
}

从代码中可以知道,isPlatformReservedTag 就是用于检测该节点类型是否是该平台的节点类型

下面来看第5点:

isDirectChildOfTemplateFor 定义如下:

// vue/compiler/optimizer.js
function isDirectChildOfTemplateFor (node: ASTElement): boolean {
  while (node.parent) {
    node = node.parent
    if (node.tag !== 'template') {
      return false
    }
    if (node.for) {
      return true
    }
  }
  return false
}

明显地从代码中看到,isDirectChildOfTemplateFor 就是检测是否是下面这种情况的:

<!-- 忽略其他,只展示关键代码 -->
<template v-for="item in [1,2,3]">
	{{ item }}
</template>

!isDirectChildOfTemplateFor(node) 说明了静态节点的父节点,不可以是带有 v-for 指令的 template 节点

下面来看第6点:

首先看一下 isStaticKey ,它是定义在最外面的。

let isStaticKey

使用到 isStaticKey 的地方是

// vue/compiler/optimizer.js
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  // ... 忽略其他代码
}

顺藤摸瓜,我们看下 genStaticKeysCached ,它也是定义到最外面的。

// vue/compiler/optimizer.js
const genStaticKeysCached = cached(genStaticKeys)

看下 genStaticKeyscached

// vue/compiler/optimizer.js
// genStaticKeys 静态属性 map 表
// makeMap 方法请看第 3 点分析贴的代码
function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
    (keys ? ',' + keys : '')
  )
}

// vue/shared/util.js
// cached 缓存方法执行结果
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

由此,可以得知,Object.keys(node).every(isStaticKey) 的意义在于检测节点属性是否是静态属性

优化

// vue/compiler/optimizer.js
/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 *
 * Once we detect these sub-trees, we can:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */

为什么 React 没有做静态节点的优化

思考这个问题之前,先思考另一个问题:为什么 Vue 可以做到静态节点的优化?

我们都知道,VueReact 最显著的区别就是:Vue 是模版化开发,React是使用 jsx 来编写的。

Vue 也是可以使用 jsx 来开发的,如官方文档中 cn.vuejs.org/v2/guide/re… 所示:

Vue.component('anchored-heading', {
 render: function (createElement) {
   return createElement(
     'h' + this.level,   // 标签名称
     this.$slots.default // 子节点数组
   )
 },
 props: {
   level: {
     type: Number,
     required: true
   }
 }
})

如上所示,render 方法中调用了 createElement 方法来替代 template 的写法。

模版转化为 vnode 的流程如下:

template -> AST -> parse -> optimize -> generate -> render -> vnode

jsx 的写法,得到 vnode 的流程如下:

render -> vnode

前面提到的静态节点的优化,如果使用 jsx 来写的话,就享受不了了呢。

所以 template 的存在,对于静态节点的识别和优化,有天然的优势。也不能肯定地说 jsx 做不到这种优化,只是,这是一种可选择优化方向,需要衡量利好和代价。

React 优化方向跟 Vue 不同,它将目光投在如何更快地响应浏览器上,创造性地使用时间切片,分优先级等,解决大量元素渲染或元素层级深嵌套渲染时导致页面卡顿或卡死的问题。

那么 Vue 要不要也用上时间切片,分优先级处理渲染的这些优化方案呢?这就需要考虑利好和代价之间的权衡了。