published: true date: 2022-2-3 tags: '前端框架 Vue'
Vue3 编译器
本章主要介绍Vue3编译器的作用,这个编译器是如何提高性能的,静态dom与动态dom的不同处理,缓存的使用以及块的作用。
致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)
编译器基本作用
<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的优势:尝试让框架变得聪明--增加了一些提示以及如何实现这一目标的优化;
回到块的想法中,稍微修改一点变为一个好的例子,加入{{msg}}使整颗树变为动态的,使其不能被提升
理想情况下,我们知道这个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。将所有这些结合起来,编译器将真正生成运行时渲染函数,它运行运行时利用所有的这些提示做尽可能少的工作