当前位置: 首页 > news >正文

18.编译优化

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

1.传统diff算法的问题

模板

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

这段节点中,可能发生变化的就是p标签,也就是说,当text值发生变化时,最高效的是直接设置p的标签内容。但传统算法需要一层一层的向下对比。

所以,如果能直接跳过这些操作,就可以大大提升性能。关键在于区分动态内容和静态内容,根据不同的内容采用不同的策略。

vue3的编译器会将编译时得到的关键信息"附着"在它生成的虚拟DOM上,这些信息都会通过虚拟DOM传递给浏览器。最终,渲染器会根据这些关键信息执行"快捷路径",从而提升运行时的性能。

2.Block 与 PatchFlags

模板

 <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 },]}

传统的虚拟 DOM 中没有任何标志能够体现出节点的动态性。但经过编译优化之后,编译器会将它提取到的关键信息“附着”到虚拟 DOM 节点上

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

描述p标签的虚拟节点拥有一个额外的属性patchFlag,值是一个数字,只要该值存在,就代表是动态节点。

定义标识的映射:

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

上述模板创建虚拟节点:

 vnode = {tag: 'div',children: [{ tag: 'div', children: 'foo' },{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }  // 这是动态节点],// 将 children 中的动态节点提取到 dynamicChildren 数组中dynamicChildren: [// p 标签具有 patchFlag 属性,因此它是动态节点{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }]}

与普通虚拟节点相比,多出了一个dynamicChildren属性,把带有该属性的虚拟节点称为"块"–Block。

Block本质是一个虚拟节点,不过比普通虚拟节点多出一个用来存储动态子节点的dynamicChildren属性,一个Block不仅能够收集它的直接动态子节点,还能收集所有的动态子代节点。

模板:

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

对应的块:

vnode = {tag: 'div',children: [{tag: 'div',children: [{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }  // 这是动态节点]},],dynamicChildren: [// Block 可以收集所有动态子代节点{ tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT }]}

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

3.收集动态节点

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

在渲染函数内,对createVnode函数的调用是层级的嵌套结构,该函数执行的顺序是"内层先执行,外层后执行"

1732094729342

为了让外层的Block节点能够收集到内层动态节点,就需要一个栈结构的数据来临时存储内层的动态节点:

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

调整createVNode函数:

function createVNode(tag, props, children, flags) {const key = props && props.keyprops && delete props.keyconst vnode = {tag,props,children,key,patchFlags: flags}if (typeof flags !== 'undefined' && currentDynamicChildren) {// 动态节点,将其添加到当前动态节点集合中currentDynamicChildren.push(vnode)}return vnode}
4.渲染器的运行时支持

有了 dynamicChildren 之后,patchElement函数内可以直接对比动态节点

function patchElement(n1, n2) {const el = n2.el = n1.elconst oldProps = n1.propsconst 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])}}

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

动态节点集合能够使得渲染器在执行更新时跳过静态节点,但对于单个动态节点的更新来说,由于它存在对应的补丁标志,因此我们可以针对性地完成靶向更新。

 function patchElement(n1, n2) {const el = n2.el = n1.elconst oldProps = n1.propsconst newProps = n2.propsif (n2.patchFlags) {// 靶向更新if (n2.patchFlags === 1) {// 只需要更新 class} else if (n2.patchFlags === 2) {// 只需要更新 style} else if (...) {// ...}} else {// 全量更新for (const key in newProps) {if (newProps[key] !== oldProps[key]) {patchProps(el, key, oldProps[key], newProps[key])}}for (const key in oldProps) {if (!(key in newProps)) {patchProps(el, key, oldProps[key], null)}}}// 在处理 children 时,调用 patchChildren 函数patchChildren(n1, n2, el)}

在 patchElement 函数内,我们通过检测补丁标志实现了 props 的靶向更新。这样就避免了全量的 props 更新,从而最大化地提升性能。

2.Block树

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

模板

 <div><section v-if="foo"><p>{{ a }}</p></section><div v-else><p>{{ a }}</p></div></div>

block收集到的动态节点

const block = {tag: 'div',dynamicChildren: [{ tag: 'p', children: ctx.a, patchFlags: 1 }]// ...}

在模板中,v-if的是section标签,v-else是div标签。前后标签不同,如果不做任何更新,将产生严重的bug:

<div><section v-if="foo"><p>{{ a }}</p></section><section v-else> <!-- 即使这里是 section --><div> <!-- 这个 div 标签在 Diff 过程中被忽略 --><p>{{ a }}</p></div></section ></div>

即使带有 v-if 指令的标签与带有 v-else 指令的标签都是 <section> 标签,但由于两个分支的虚拟 DOM 树的结构不同,仍然会导致更新失败。

原因在于,dynamicChildren 数组中收集的动态节点是忽略虚拟 DOM 树层级的。换句话说,结构化指令会导致更新前后模板的结构发生变化,即模板结构不稳定。

解决方法:只需要让带有 v-if/v-else-if/v-else 等结构化指令的节点也作为 Block 角色即可。

模板

 <div><section v-if="foo"><p>{{ a }}</p></section><section v-else> <!-- 即使这里是 section --><div> <!-- 这个 div 标签在 Diff 过程中被忽略 --><p>{{ a }}</p></div></section ></div>

如果上面这段模板中的两个 <section> 标签都作为 Block 角色,那么将构成一棵 Block 树:

Block(Div)

- Block(Section v-if)- Block(Section v-else)

父级 Block 除了会收集动态子代节点之外,也会收集子 Block。因此,两个子 Block(section) 将作为父级 Block(div) 的动态节点被收集到父级 Block(div) 的 dynamicChildren 数组中

 block = {tag: 'div',dynamicChildren: [/* Block(Section v-if) 或者 Block(Section v-else) */{ tag: 'section', { key: 0 /* key 值会根据不同的 Block 而发生变化 */ }, dynamicChildren: [...]},]}

这样,当 v-if 条件为真时,父级 Block 的 dynamicChildren 数组中包含的是 Block(section v-if);当v-if 的条件为假时,父级 Block 的 dynamicChildren 数组中包含的将是 Block(section v-else)。在 Diff 过程中,渲染器能够根据 Block 的 key 值区分出更新前后的两个 Block 是不同的,并使用新的 Block 替换旧的 Block。这样就解决了 DOM 结构不稳定引起的更新问题。

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

模板

 <div><p v-for="item in list">{{ item }}</p><i>{{ foo }}</i><i>{{ bar }}</i></div>

假设list本身为[1,2],更新为[1]

// 更新前const prevBlock = {tag: 'div',dynamicChildren: [{ tag: 'p', children: 1, 1 /* TEXT */ },{ tag: 'p', children: 2, 1 /* TEXT */ },{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },]}// 更新后const nextBlock = {tag: 'div',dynamicChildren: [{ tag: 'p', children: item, 1 /* TEXT */ },{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },]}

进行 Diff 操作的节点必须是同层级节点。但是 dynamicChildren 数组内的节点未必是同层级的

将带有v-for指令的标签也作为block角色即可:

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 */ },]}

由于 v-for 指令渲染的是一个片段,所以我们需要使用类型为 Fragment 的节点来表达 v-for 指令的渲染结果,并作为 Block 角色。

3.Fragment的稳定性

一个Fragment节点模板

<p v-for="item in list">{{ item }}</p>

当list由[1,2]更改为[1]后,对应的block为

 // 更新前const prevBlock = {tag: Fragment,dynamicChildren: [{ tag: 'p', children: item, 1 /* TEXT */ },{ tag: 'p', children: item, 2 /* TEXT */ }]}// 更新后prevBlock = {tag: Fragment,dynamicChildren: [{ tag: 'p', children: item, 1 /* TEXT */ }]}

Fragment 本身收集的动态节点仍然面临结构不稳定的情况:从结果看,指的是更新前后一个block的dynamicChildren数组中收集的动态节点的数量或顺序不一致。这样就无法精准判断更新的结果,从而无法进行靶向更新。

解决方法:放弃根据dynamicChildren数组中的动态节点进行靶向更新的思路,并退回到传统虚拟DOM的Diff手段,即直接使用Fragment的children,而非dynamicChildren来进行 Diff 操作。

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

 const block = {tag: Fragment,children: [{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }]}
4.静态提升

模板

 <div><p>static text</p><p>{{ title }}</p></div>

没有静态提升的情况下,对应的渲染函数:

function render() {return (openBlock(), createBlock('div', null, [createVNode('p', null, 'static text'),createVNode('p', null, ctx.title, 1 /* TEXT */)]))}

一个静态的文本,一个动态的文本。当修改时,静态的文本又会被重新创建一遍,造成了不必要的性能开销,应该将纯静态的节点提升到渲染函数之外,就是静态提升:

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

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

把纯静态的节点提升到渲染函数之外后,在渲染函数内只会持有对静态节点的引用。当响应式数据变化,并使得渲染函数重新执行时,并不会重新创建静态的虚拟节点,从而避免了额外的性能开销。

 //静态提升是以树为单位//模板:
<div><section><p><span>abc</span></p></section ></div>

除了div作为Block角色不可被提升之外,整个section元素即子节点都会被提升。如果span的内容是动态内容,则都不会提升。

模板:

<div><p foo="bar" a=b>{{ text }}</p></div>

p 标签存在动态绑定的文本内容,因此整个节点都不会被静态提升。但该节点的所有 props 都是静态的,因此在最终生成渲染函数时,我们可以将纯静态的 props 提升到渲染函数之外。

静态提升的 props 对象

 const hoistProp = { foo: 'bar', a: 'b' }function render(ctx) {return (openBlock(), createBlock('div', null, [createVNode('p', hoistProp, ctx.text)]))}
5.预字符串化

静态的虚拟节点或虚拟节点树本身都是静态的

如下模板:

 <div><p></p><p></p>// ... 20 个 p 标签<p></p></div>

假设模板中包含大量纯静态节点,采用静态提升后,编译代码如下:

 cosnt hoist1 = createVNode('p', null, null, PatchFlags.HOISTED)cosnt hoist2 = createVNode('p', null, null, PatchFlags.HOISTED)// ... 20 个 hoistx 变量cosnt hoist20 = createVNode('p', null, null, PatchFlags.HOISTED)render() {return (openBlock(), createBlock('div', null, [hoist1, hoist2, /* ...20 个变量 */, hoist20]))}

预字符串化能让静态节点序列化为字符串,生成一个静态类型的VNode

 const hoistStatic = createStaticVNode('<p></p><p></p><p></p>...20 个...<p></p>')render() {return (openBlock(), createBlock('div', null, [hoistStatic]))}

优势:

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

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

● 减少内存占用。

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

模板:

<Comp @change="a + b" />

编译器会创建一个内联事件处理函数:

function render(ctx) {return h(Comp, {// 内联事件处理函数onChange: () => (ctx.a + ctx.b)})}

不过,每次render重新执行时,都会为Comp组件创建一个全新的props对象。同时,props对象中onChange属性的值也会是全新的函数。这会导致渲染器对Comp组件进行更新,造成新能开销。为了避免这类无用的更新,可以对内联事件处理函数进行缓存

function render(ctx, cache) {return h(Comp, {// 将内联事件处理函数缓存到 cache 数组中onChange: cache[0] || (cache[0] = ($event) => (ctx.a + ctx.b))})}

渲染函数的第二个参数是一个数组 cache,该数组来自组件实例,我们可以把内联事件处理函数添加到 cache 数组中。这样,当渲染函数重新执行并创建新的虚拟 DOM 树时,会优先读取缓存中的事件处理函数。这样,无论执行多少次渲染函数,props 对象中 onChange 属性的值始终不变,于是就不会触发 Comp 组件更新了。

7.v-once

缓存事件内联还可以配合实现v-once对虚拟DOM的缓存

模板:

 <section><div v-once>{{ foo }}</div></section>

div 标签存在动态绑定的文本内容。但是它被 v-once 指令标记,所以这段模板会被编译为:

 function render(ctx, cache) {return (openBlock(), createBlock('div', null, [cache[1] || (cache[1] = createVNode("div", null, ctx.foo, 1 /* TEXT */))]))}

在上面的编译结果中,div对应的虚拟节点被缓存到了cache数组中。既然虚拟节点已经被缓存了,那么后续更新导致渲染函数重新执行时,会优先读取缓存的内容,而不会重新创建虚拟节点。

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

 render(ctx, cache) {return (openBlock(), createBlock('div', null, [cache[1] || (setBlockTracking(-1), // 阻止这段 VNode 被 Block 收集cache[1] = h("div", null, ctx.foo, 1 /* TEXT */),setBlockTracking(1), // 恢复cache[1] // 整个表达式的值)]))}

setBlockTracking(-1) 函数调用,它用来暂停动态节点的收集。换句话说,使用 v-once 包裹的动态节点不会被父级 Block 收集。因此,被 v-once 包裹的动态节点在组件更新时,自然不会参与 Diff 操作。

v-once 指令能够从两个方面提升性能:

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

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

总结

1732497127749

1732497139140

http://www.dtcms.com/a/306026.html

相关文章:

  • SQL167 连续签到领金币
  • MySQL 9 Group Replication维护
  • 达梦数据库(DM Database)角色管理详解|了解DM预定义的各种角色,掌握角色创建、角色的分配和回收
  • C++:STL中list的使用和模拟实现
  • Keepalived 实战
  • 《C++二叉搜索树原理剖析:从原理到高效实现教学》
  • 如何利用 Redis 的原子操作(INCR, DECR)实现分布式计数器?
  • Java 控制台用户登录系统(支持角色权限与自定义异常处理)
  • 生成模型实战 | GLOW详解与实现
  • 从理论到实践:全面解析机器学习与 scikit-learn 工具
  • 汽车、航空航天、适用工业虚拟装配解决方案
  • 关于“PromptPilot” 之4 -目标系统软件架构: AI操作系统设计
  • 第六章:进入Redis的List核心
  • 【8月优质EI会议合集|高录用|EI检索稳定】计算机、光学、通信技术、电子、建模、数学、通信工程...
  • 人工智能与家庭:智能家居的便捷与隐患
  • 移动端WebView调试实战 全面排查渲染性能与布局跳动问题
  • ISO 26262 汽车功能安全(腾讯混元)
  • MongoDB系列教程-第二章:MongoDB数据库概念和特点、数据库操作、集合操作、文档操作、规范及常见问题解决、实际应用示例
  • JXD进步25.7.30
  • Thales靶机
  • 《Vuejs设计与实现》第 12 章(组件实现原理 下)
  • 非凸科技受邀出席第九届AIFOF投资创新发展论坛
  • 前端安全防护:XSS、CSRF与SQL注入漏洞深度解析与防御
  • 亚马逊云科技:赋能企业数字化转型,解决实际发展难题
  • 【Axure高保真原型】轮播条形图
  • 让科技之光,温暖银龄岁月——智绅科技“智慧养老进社区”星城国际站温情纪实
  • 【HarmonyOS】鸿蒙应用HTTPDNS 服务集成详解
  • 【Lua】元表常用属性
  • 【MySQL】MySQL索引—B树/B+树
  • 【选型】HK32L088 与 STM32F0/L0 系列 MCU 参数对比与选型建议(ST 原厂 vs 国产芯片)