首页>>前端>>Vue->浅谈vue3的编译优化

浅谈vue3的编译优化

时间:2023-12-01 本站 点击:0

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

优化的方向:尽可能地区分动态内容和静态内容,并针对不同的内容采用不同的优化策略

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

1.1 传统diff算法的问题

比对新旧两棵虚拟DOM树的时候,总是要按照虚拟DOM的层级结构“一层一层”地遍历

<div id="foo">    <p class="bar">{{ text }}</p></div>

上面这段代码中,当响应式数据text值发生变化的时候,最高效的更新方式是直接设置p标签的文本内容

传统Diff算法做不到如此高效,当text值发生变化的时候,会产生一颗新的虚拟DOM树,对比新旧虚拟DOM过程如下:

对比div节点,以及该节点的属性和子节点

对比p节点,以及该节点的属性和子节点

对比p节点的文本子节点,如果文本子节点的内容变了,则更新,否则什么都不做

可以发现,有很多无意义的对比操作。

总结:

传统diff算法的问题: 无法利用编译时提取到的任何关键信息,导致渲染器在运行时不会去做相关的优化。

vue3的编译器会将编译得到的关键信息“附着”在它生成的虚拟DOM上,传递给渲染器,执行“快捷路径”。

1.2 Block 与 PatchFlags

传统Diff算法无法避免新旧虚拟DOM树间无用的比较操作,是因为运行时得不到足够的关键信息,从而无法区分动态内容和静态内容。 换句话说,只要运行时能够区分动态内容和静态内容,就可以实现极简的优化策略

举个例子:

<div>    <div>foo</div>    <p>{{ bar }}</p></div>

只有 {{ bar }}是动态的内容。理想情况下,当数据bar的值变化时,只需要更新p标签的文本节点即可。为了实现这个目标,需要提供信息给运行时

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}

可以发现,虚拟节点多了一个额外的属性,即 patchFlag(补丁标志),存在该属性,就认为是动态节点

patchFlag(补丁标志)可以理解为一系列的数字标记,含义如下

const PatchFlags = {  TEXT: 1, // 代表节点有动态的 textContent  CLASS: 2, // 代表元素有动态的 class 绑定  STYLE: 3  // 其他。。。}

可以在虚拟节点的创建阶段,把它的动态子节点提取出来,并存储到该虚拟节点的 dynamicChildren 数组中

const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ],  // 将children 中的动态节点提取到 dynamicChildren 数组中  dynamicChildren: [    { tag: 'p', children: ctx.bar, patchFlag:  PatchFlags.TEXT }  ]}

Block定义: 带有 dynamicChildren 属性的虚拟节点称为“块” ,即(Block)

一个Block本质上也是一个虚拟DOM, 比普通的虚拟节点多处一个用来存储动态节点的 dynamicChildren属性。(能够收集所有的动态子代节点)

渲染器的更新操作会以Block为维度。当渲染器在更新一个Block时,会忽略虚拟节点的children数组,直接找到dynamicChildren数组,并只更新该数组中的动态节点。跳过了静态内容,只更新动态内容。同时,由于存在对应的补丁标志,也能够做到靶向更新。

Block节点有哪些: 模版根节点、 带有v-for、v-if/v-else-if/v-else等指令的节点

1.3 收集动态节点

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

render() {  return createVNode('div', { id: 'foo' }, [    createVNode('p', null, 'text')  ])}function createVNode(tag, props, children) {  const key = props && props.key  props && delete props.key  // 省略部分代码  return {    tag,    props,    children,    key  }}

createVNode的返回值是一个虚拟DOM节点

举个例子:

<div id="foo">  <p class="bar">{{ bar }}</p></div>

上面模版生成带有补丁标志的渲染函数如下:

render() {  return createVNode('div', { id: 'foo' }, [    createVNode('p', { class: 'bar' }, text, PatchFlags.TEXT)  ])}

怎么将根节点变成一个Block, 如何将动态子代节点收集到该Block的dynamicChildren数组中?

可以发现,在渲染函数内,对createVNode函数的调用是层层嵌套结构,执行顺序是 内层先执行,外层再执行, 当外层createVNode函数执行时,内层的createVNode函数已经执行完毕了。因此,为了让外层Block节点能够收集到内层动态节点,需要一个栈结构的数据来临时存储内层的动态节点。 代码实现如下:

// 动态节点const dynamicChildrenStack = []// 当前动态节点集合let currentDynamicChildren = null// openBlock 用来创建一个新的动态节点集合,并将该集合压入栈中function openBlock() {  dynamicChildrenStack.push((currentDynamicChildren = []))}// closeBlock 用来通过openBlock创建的动态节点集合从栈中弹出function closeBlock() {  currentDynamicChildren = dynamicChildrenStack.pop()}

然后调整createVNode函数

<div>    <div>foo</div>    <p>{{ bar }}</p></div>0

接着调整

<div>    <div>foo</div>    <p>{{ bar }}</p></div>1

1.4.渲染器的运行时支持

传统的节点更新方式如下:

<div>    <div>foo</div>    <p>{{ bar }}</p></div>2

优化后的更新方式,直接对比动态节点

<div>    <div>foo</div>    <p>{{ bar }}</p></div>3

存在对应的补丁标志,可以针对性地完成靶向更新

<div>    <div>foo</div>    <p>{{ bar }}</p></div>4

2. Block树

除了模版的根节点是Block外, 带有结构化指令的节点,如:v-if、v-for,也都应该是Block

2.1 带有v-if指令的节点

<div>    <div>foo</div>    <p>{{ bar }}</p></div>5

假设只有最外层的div标签会作为Block, 那么变量foo的值为true还是false, block收集到的动态节点都是一样的,如下:

<div>    <div>foo</div>    <p>{{ bar }}</p></div>6

这意味着,在Diff阶段不会更新。显然,foo 不同值下,一个是 section, 一个是 div, 是不同标签,是需要更新的。

再举个例子:

<div>    <div>foo</div>    <p>{{ bar }}</p></div>7

一样会导致更新失败

问题在于:dynamicChildren收集的动态节点是忽略虚拟DOM树层级的,结构化指令会导致更新前后模版的结构发生变化,即模版结构不稳定

解决方法: 让带有v-if/v-else-if/v-else等结构化指令的节点也作为Block即可,如下所示

<div>    <div>foo</div>    <p>{{ bar }}</p></div>8

<div>    <div>foo</div>    <p>{{ bar }}</p></div>9

在Diff过程中, 渲染器根据key值区分, 使用新的 Block 替换旧的 Block

2.2 带有 v-for 指令的节点

带有 v-for 指令的节点也会让虚拟DOM树变得不稳定

例子:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}0

list 的值 由 [1, 2] 变成 [1]

更新前后对应的 Block 树如下:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}1

更新前后,动态节点数量不一致,无法进行 diff 操作(diff操作的前提是:操作的节点必须是同层级节点, dynamicChildren不一定是同层级的

解决方法: 让 v-for指令的标签也作为Block角色,保证虚拟DOM树具有稳定的结构,无论 v-for 在运行时怎样变化。如下:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}2

由于 v-for指令渲染的是一个片段,所以类型用 Fragment

2.3 Fragment的稳定性

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}3

发现 Fragment 本身收集的动态节点存在结构是不稳定的情况

结构不稳定: 指更新前后一个block的dynamicChildren数组中收集的动态节点的数量或顺序不一致

这种情况无法直接进行靶向更新

解决方法: 回退到传统虚拟DOM的Diff手段,即直接使用Fragment的children而非 dynamicChildren来进行Diff操作

Fragment 的子节点仍然可以是由 Block 组成的数组

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}4

当Fragment 的子节点更新时,就可以恢复优化模式

有稳定的Fragment吗? 如下:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}5

稳定的Fragment, 可以使用优化模式

vue3模版中的多个根节点,也是稳定的Fragment

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}6

3. 静态提升

减少更新时创建虚拟DOM带来的性能开销和内存占用

如:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}7

没有静态提升时,渲染函数是:

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}8

响应式数据 title 变化后,整个渲染函数会重新执行

把纯静态的节点提升到渲染函数之外

// 传统虚拟DOM描述const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar },  ]}9

响应式数据 title 变化后,不会重新创建静态的虚拟节点

注: 静态提升是以树为单位的

包含动态绑定的节点本身不会被提升,但是该节点上的静态属性是可以被提升的

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}0

可以减少创建虚拟DOM产生的开销以及内存占用

4. 预字符串化

基于静态提升,进一步采用预字符串化优化。

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}1

采用静态提升优化策略后

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}2

采用预字符串化将这些静态节点序列化为字符串, 并生成一个Static类型的VNode

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}3

优势:

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

减少创建虚拟节点产生的性能开销

减少内存占用

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

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}4

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}5

每次重新渲染时,都会为Com组件创建一个全新的props对象。同时,props对象中onChange属性的值也会是全新的函数。造成额外的性能开销

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}6

6. v-once

v-once 可以对虚拟DOM进行缓存

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}7

由于节点被缓存,意味着更新前后的虚拟节点不会发生变化,因此也就不需要这些被缓存的虚拟节点参与Diff操作了。编译后的结果如下:

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}8

v-once包裹的动态节点不会被父级Block收集,因此不会参与Diff操作

v-once指令通常用于不会发生改变的动态绑定中,例如绑定一个常量

// 编译优化后const vnode = {  tag: 'div',  children: [    { tag: 'div', children: 'foo' },    { tag: 'p', children: ctx.bar, patchFlag: 1 }, // 这是动态节点  ]}9

v-once带来的性能提升

避免组件更新时重新创建虚拟DOM带来的性能开销。因为虚拟DOM被缓存了, 所以更新时无需重新创建

避免无用的Diff开销。因为被v-once标记的虚拟DOM树不会被父级Block节点收集

7. 总结

1. vue3提出了 Block 的概念, 利用 Block树及补丁标志

2. 静态提升:可以减少更新时创建虚拟DOM产生的性能开销和内存占用

3. 预字符串化: 在静态提升的基础上,对静态节点进行字符串化。这样做能够减少创建虚拟节点产生的性能开销以及内存占用

4. 缓存内联事件处理函数:避免造成不必要的组件更新

5. v-once指令: 缓存全部或部分虚拟节点,能够避免组件更新时重新创建虚拟DOM带来的性能开销, 也可以避免无用的Diff操作

原文:https://juejin.cn/post/7101859824203202568


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/5052.html