Vue3 编译器
编译器基本作用
<div>foo</div>
上面是源模板,下面是生成的渲染函数代码
{
export function ssrRender(_ctx, _push, _parent) {
_push(`<div>foo</div>`)
}
}
有什么作用:在开发过程中,用这个来调试编译器,知道实际是怎么运行的;
静态优化选项
右上角的是一些优化选项,这里都是静态的,所以可以打开这个选项:
然后对应的编译代码也会发生变化:
以便在每次组件更新的时候可以在每个渲染器上重用它,一旦一个节点被提升,它就会被创建一次在渲染函数之外,在以后的每一次渲染中,它将在这里重新使用,两个好处:
- 避免重新创建对象,然后扔掉(垃圾回收相关的知识)
- 在模式算法中,当看到两个节点在同一位置时,在严格平等的情况下,可以跳过它,因为我们知道它永远不会改变
动态相关优化
这里绑定了 click 侦听器,编译器会生成一个补丁标志
通常使用简单的虚拟 DOM 渲染算法,不管有多少东西在 div 自身上
这整个对象必须作为一个整体来 diff:
所以即使从模板中我们可以看到这个 ID 实际上是静态的,永远不会改变,我们还是会遍历整个对象,只是为了确保它不会改变,因为运行时没有足够的信息来知道这方面;
但是,使用 Vue3 的编译器,这个补丁和数组结合在一起,为运行提供足够的信息<表示有些 props 会改变,但唯一可能改变的是 onClick,因为它适合暴露在外的东西结合在一起>
所以我们可以跳过此 props 上的对象枚举,忽略那些已经被编译器推断永远不会改变的 props
总的,性能优化的方面做了许多的更新,在虚拟 DOM 中,当情况发生变化时,并没有检查整个节点的所有属性和元素,检查的是一些具体的东西通过添加类似的提示<确切地说,这些是编译器生成的提示,以帮助运行时更高效>
但是很多时候,你并不打算改变事件处理程序,所以有一个选项是默认打开的:
这里使用了一些智能 JavaScript 来缓存事件处理程序,这里将它变成了一个内联函数,并在第一次渲染时将其缓存,后续渲染就始终使用同一个内联处理程序了,所以我们总是传递相同的函数,但是这里面的函数会访问 ctx.onClick
**重点:**我们注意到补丁标志与 onClick 数组不见了,这意味着现在这个 vnode 当我们试图修补它的时候,它实际上并不需要被修补,因为这(id: “foo”)是静态的,而事件处理程序也已经被缓存,当被调用时,它总是指向最新的 onClick,所以即使 onClick 下面发生了变化,我们不需要对 vnode 本身做任何事情,可以理解为 vnode 里面存的是指向,具体的程序存在缓存中,并且实时更新为最新的。所以,现在我们在修补过程中可以完全跳过整个节点。
这一点非常重要,因为在组件中,如果要将事件处理程序添加到组件中,会导致子组件不必要地重新渲染的最常见的情况之一是指使用类似内联事件处理程序
或者当你些 foo 的时候,你给它一个参数,这也是一个隐式的内联处理程序
所以这些在 Vue2 中,即使什么都没有改变,它仍然会导致子组件在父组件重新渲染时而重新渲染,在大型应用,这会引起连锁反应,因为你在向下传递函数,在每次渲染时,都会创建一个新的内联函数,会导致所有这些收到那个 prop 的子组件重新渲染;
所以在 Vue3 中使用处理缓存,极大地减少了在大型组件树中发生不必要的渲染
block 有什么作用?
当根 div 被创建时,就像 block 一样:
假如有一个这样的临时结构:
在右边,我们可以看到它被提升了,_hoisted_1
想象这是一个手动写的非优化虚拟 DOM 树,在更新时,你要确保 DOM 结构是一致的,如果这是手动编写的,那么运行时就没有关于这个 DOM 树是否稳定的信息了,它不能做出仍任何假设因为节点顺序可能已经改变了或者 div-->p,所以运行时需格外小心,它必须检查每个节点以确保它没有变成别的东西,如果有 props 的话就要把所有的 props 都区分开来确保 props 没有改变,而说孩子节点,事实上,它必须区分两个子数组,以确保它们没有四处走动或者没有新的孩子节点加入或删除。
<div>
<div>
<span>hello</span>
</div>
</div>
会得到这样的结果:
function render(){
return h('div', [
h('div', [
h('span', 'hello')
])
])
}
你可以想象最终的 Json 结构,底层数据结构可能是这个样子
const vdom = {
tag: 'div',
children: [
{
tag: 'div',
children: [
{
tag: 'span',
children: 'hello' // 当这里变化为 msg(#)
}
]
}
]
}
像这样的结构,更新时,它将有两个快照(新旧)
“#”部分: 如果我们不提供更多的提示,渲染器并不知道发生了什么变化,所以上述结构必须经过一个相对暴力的算法递归遍历整颗树,比较新旧;
对于中小型应用,这种方法并不会达到性能瓶颈,但是对于大型应用:当你点击某个东西时,可能你的应用程序会有 10 个组件同时被触发再更新,这就是 JavaScript 成本开始增加的时候,可能阻塞或卡顿。这时候人们就开始了解如何手动优化组件树来避免不必要的重新渲染,这就是 Vue 的优势:尝试让框架变得聪明--增加了一些提示以及如何实现这一目标的优化;
回到块的想法中,稍微修改一点变为一个好的例子,加入使整颗树变为动态的,使其不能被提升
理想情况下,我们知道这个 div 不会改变,唯一可能改变的就是这个 span;
如果在其他地方添加一些不相关的节点
作为人类,我们可以很清楚的知道整颗 DOM 树只有 span 那个节点在变化,但是如果没有编译器生成的提示,虚拟 DOM 渲染器只看到 JavaScript 树,它并不知道哪个部分会改变,所以编译器的工作就是提供这些信息,运行的时候就知道可以跳过很多不必要的工作,这里就是直接到 span 这里
使用的方法就是 block,将模板的根变成 block
注意这里有一个 openBlock 调用,当块打开时,所有表达式、所有的孩子会被评估,这是在欺骗 JavaScript,想法是当你创建这样一个节点时
因为它是动态的,它有我们称之为补丁标志的东西,应该被跟踪,当我们说 tracked 时,这个节点就会被添加到当前打开的 block 作为动态节点。所以整个调用之后,这个根 div 将有一个额外的属性称为动态子节点,它将只包含此节点,同时我们还有完整的结构通过正常的子层级,但是每个 block 都有一个额外的数组,只跟踪其中的动态节点,所以这个 span 标签可以无论多深,block 将只跟踪动态节点在一个扁平数组中。
使用 v-if 等结构指令
可能改变节点的结构
当这个被切换时,整个 div 就会从树上消失,所以对于这个根 block,它再也不能对此做出安全的假设了,相反,我们把这个完整的 if 部分变成一个块(它自己的)这个块被跟踪了,作为父块的动态子级。所以我们有嵌套的块,每个块将在扁平的数组中跟踪它自己的动态子对象
数个节点中可能只有几个这样的 v-if 和 v-for,所以,我们基本上还是需要遍历 block 树。然而,大多数情况下是扁平数组迭代,而不是去 diff 和比较检查潜在的节点移动,所以效率更高,减少了递归的数量;
对于每个节点,补丁标志本身还编码了关于什么样的工作的信息,比如上面这个 TEXT 标志意味着:当你试图区分这个节点时,你只需检查它的文本内容,而不必考虑它的 props。将所有这些结合起来,编译器将真正生成运行时渲染函数,它运行运行时利用所有的这些提示做尽可能少的工作