14-编译优化

31 阅读5分钟

编译优化:指的是编译器将模板编译为渲染函数的过程中尽可能多提取关键信息,并以此指导生成最优代码的过程。

1、动态节点收集与补丁标志

1、传统Diff算法的问题

Diff算法需要按照虚拟DOM的层级结构进行一层一层的比较(不高效)

传统Diff算法无法利用编译时提取有效信息

2、Block与PatchFlags

在描述标签的虚拟节点拥有一个额外的属性PatchFlag(补丁标志),它的值是数字,有了这项信息可以在虚拟节点的创建阶段,把它的动态子节点取出来,并存储到该虚拟节点的dynamicChildren数组内

3、收集动态节点

在编译器生成的渲染函数代码中,并不会直接包含用来描述虚拟节点的数据结构,而是包含创建虚拟DOM节点的辅助函数

其中createVNode创建虚拟节点,编译器在优化阶段,会生成带有补丁标志的渲染函数

createVNode函数内部,检测节点是否存在补丁标志,如果存在,则说明该节点是动态节点,需要将动态节点压入栈中。

4、渲染器的运行时支持

当有了动态节点集合之后和补丁标志,基于这两点渲染器可实现靶向更新

patchElement需要加入动态节点的对比,代码如下

function patchElement(n1, n2) {
  const el = n2.el = n1.el
  const oldProps = n1.props
  const newProps = n2.props

  // 省略部分代码

  if (n2.dynamicChildren) {
    // 调用 patchBlockChildren 函数,这样只会更新动态节点
    patchBlockChildren(n1, n2)
  } else {
    patchChildren(n1, n2, el)
  }
}

function patchBlockChildren(n1, n2) {
  // 只更新动态节点即可
  for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
  }
}

修改后,优先检测虚拟DOM是否存在动态节点集合,dynamicChildren数组,如果存在,则直接调用patchBlockChildren函数完成更新,这样就会跳过所有静态节点,只更新动态节点。

2、Block树

Block树中除了根节点,还需要其他特殊节点充当block角色,比如v-if和v-for

1、带有v-if指令的节点

当v-if条件为true时,父级block的dynamicChildren包含的是Block(section v-if);当v-if的条件为false时,父级Block的dynamicChildren数组中包含的将是Block(section v-else)。

在Diff过程中,渲染器能根据Block的可以区分出更新前后的两个Block是不同的,使用新Block替换旧Block,可以解决DOM结构不稳定引起的更新问题。

2、带有v-for指令的节点

v-for也存在不稳定的问题,解决的办法就是带有v-for指令的标签也作为block角色

由于v-for指令渲染的是一个片段,需要使用类型为Fragment的节点来表达v-for指令的渲染结果

(大致这个样子)

 const block = {
   tag: 'div',
   dynamicChildren: [
     // 这是一个 Block,它有 dynamicChildren
     { tag: Fragment, dynamicChildren: [/* v-for 的节点 */] }
     { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
     { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
   ]
 }

3、Fragment的稳定性

用Fragment可以解决v-for指令渲染不稳定的问题,接下来深入学习Fragment节点本身,Fragment本身收集动态节点也有不稳定的情况,需要退回传统的Diff算法进行比较

3、静态提升

编译优化的重要部分:静态提升,可以减少更新时创建虚拟DOM性能开销和内存占用

静态提升:就是把静态的节点从渲染函数里面拿出来放到外面,渲染函数只对静态节点进行引用不会重复创建。(就是这个样子)

// 把静态节点提升到渲染函数之外
const hoist1 = createVNode('p', null, 'text')

function render() {
  return (openBlock(), createBlock('div', null, [
    hoist1, // 静态节点引用
    createVNode('p', null, ctx.title, 1 /* TEXT */)
  ]))
}

4、预字符串化

静态提升的虚拟节点,本身就是静态,可以把静态节点序列化为字符串,并生成一个Static类型的VNode

优点:

大块的静态内容可以通过innerHTML进行设置,性能上有一定优势

减少内存占用

5、缓存内联事件处理函数

内联事件处理函数放入缓存cache数组中,可以在创建新的虚拟DOM树时优先读取缓存中的,事件处理函数。

6、v-once

v-once标记后,标签对应的虚拟节点会被缓存,缓存的虚拟节点不需要再参加Diff算法

v-once标记会有两个性能提升:

1、避免组件更新时重新创建虚拟DOM带来的性能开销

2、避免无用的Diff开销,v-once标记后不会被父级Block节点收集

总结

1、编译优化主要是区分静态和动态虚拟节点,dynamicChildren数组保存动态节点,之后操作处理这个数组

2、Block树,收集所有动态子代节点,忽略DOM层级结构

3、静态提升,静态节点,提出渲染函数之外,避免重复创建

4、预字符串化,在静态提升基础之上,对静态节点进行字符串化,减少内存开销

5、缓存内联事件处理函数,避免造成不必要的组件更新

6、v-once,缓存部分或全部虚拟节点,避免组件更新重新创建虚拟DOM的性能开销,也可避免无用的Diff操作