揭秘!Vue3.5照应式重构如何让内存占用缩小56%
前言
Vue3.5版本又将照应式给重构了,重构后的照应式系统关键有两局部组成:双向链表和 版本计数。咱们在前两篇文章中咱们曾经讲过了 双向链表和 版本计数,这篇文章咱们来讲讲为什么这次重构能够让内存占用缩小56%。
为什么说“又”将照应式重构了
由于在之前的Vue3.4版本中刚刚将照应式给重构了,这次照应式重构是vscode插件Vue-Official(原名Volar)的作者Johnson Chu搞的。
3.4版本的重构优化了很多物品,最直观的就是:computed计算属性的值没有变动,另外一个watch又监听了这个computed的值。在3.4以前还是会触发watch的回调,经过3.4的优化后就不会触发了。
在3.5版本以前,Vue的照应式系统中有两个角色:Sub订阅者和Dep依赖。
Sub订阅者:关键有watchEffect、watch、render函数、computed等。
Dep依赖:关键有ref、reactive、computed等照应式变量。
他们两之间是相互依赖的相关,如下图:
Dep依赖(比如ref照应式变量)可以经过dep属性访问到Sub订阅者(比如computed计算属性),就知道了究竟有哪些订阅者依赖自己,当自己的值扭转后就能去通知订阅者。
雷同Sub订阅者(比如computed计算属性)可以经过deps属性访问到Dep依赖(比如ref照应式变量),当Sub订阅者不再依赖某个变量时就可以经过这个相关去访问到这个Dep依赖。而后把自己从不再依赖的变量的Sub订阅者汇合中去掉,这样当这个照应式变量扭转后就不会通知到不再订阅到他的Sub订阅者了。
咱们来看个例子,代码如下:
templatep{{ doubleCount }}pbutton 切换flagbuttontemplatescript setup { computed ref } const count1 refconst count2 refconst flag refconst doubleCount computed {consolelog flag { count1 } { count2 }}script
当flag的值为true时计算属性doubleCount其实只依赖照应式变量flag和count1,当flag的值切换为false时,计算属性应该变成依赖变量flag和count2。
就上方这个更新Sub订阅者依赖的逻辑,Vue其实重构了很屡次。在早期的Vue3版本中是间接清空Sub订阅者所依赖的照应式变量,而后再从新口头计算属性doubleCount时再去将新的照应式变量启动搜集。很显著这个版本内存的经常使用就十分糜费了。
在最新的Vue3.4版本重构后的照应式系统中会在口头计算属性之前应用_trackId和_depsLength字段启动标志,在从新口头计算属性时启动依赖搜集就可以应用_trackId和_depsLength字段判别出Dep依赖能否能够复用,并且口头完计算属性的回调函数后雷同应用_trackId和_depsLength字段就可以将不再依赖的Dep依赖给移除掉。
上方这个打算看着很完美,然而他的外围是依赖计算属性中所依赖的变量顺序不变,假设顺序变了,那么依然还是不能够复用的,雷同会对糜费内存。(PS:这一段3.4版本照应式看疑问没相关,由于他曾经是过去式了)
内存优化关键要素:复用Link节点
在Vue3.5版本中那个最了解Vue的男人出手了,经常使用双向链表和版本计数将照应式系统再次给重构了。说瞎话这次重构后让读照应式源码的门槛变得更高了,然而收益特意显著,最关键是经过复用Link节点去成功缩小内存的经常使用。
还是上方的那个例子,对应新的照应式模型如下图:
在新的照应式模型中Sub订阅者和Dep依赖之间不再有间接的关联相关了,而是经过两边的Link节点作为桥梁去关联。
在前一节中咱们讲过了,3.5以前Sub订阅者中有属性会去存依赖的Dep依赖,Dep依赖中有属性去存依赖他的Sub订阅者,所以造成当Sub订阅者依赖的变量须要更新时就不可做到齐全的复用,内存就会糜费。
假设上方的内容你看疑问,这不是你了解力有疑问,要素是你对双向链表不相熟,可以先看看我之前的 双向链表文章。
在3.5新的照应式模型中,X轴是Dep依赖,Y轴是Sub订阅者,Link节点是作为坐标轴上方的点。每一组Dep依赖和Sub订阅者都会对应一个Link节点,并且可以经过这个Link节点间接访问到Dep依赖和Sub订阅者。
在Y轴上方找一个点(比如Sub1也就是计算属性doubleCount),横向登程就可以找到Sub1订阅者所依赖的一切照应式变量。由于横向的这些Link节点是一个双向链表,并且可以经过某一个Link节点间接访问到他的Dep依赖。
当flag的值切换为false后,订阅者Sub1所依赖的照应式变量就从flag+count1变成flag+count2。这时咱们须要做的事情就很便捷了,新建一个Link3节点,可以间接访问到Sub1和Dep3。而后将Link1中原本指向Link2的指针改为指向Link3,同时让Link3的指针也指向Link1。并且将Link2指向Link1的指针改为指向空,由于Dep2如今不被任何订阅者所依赖了,所以将Link2原本指向Dep2的指针也改为指向空,雷同将Dep2指向Link2的指针也指向空。
上方的一顿操作,除了必要的初始化一个Link3之外咱们不时都是在启动指针的操作,并不像以前的照应式一样去参与Sub订阅者依赖或许缩小依赖,这是十分高效的模式。
当flag的值切换为false后,新的照应式模型图如下:
从上图中可以看到Link2曾经彻底从双向链表中移除了,并且整个环节中咱们都是在操作指针的指向,所以Link1也不时都是复用的。
V8在启动渣滓回收的时刻发现Link2不再被任何变量所经常使用,就可以以为Link2是一个可以被回收的变量,就会将其间接回收监禁内存。
Link节点复用以及让不再经常使用的Link节点尽快的被回收进而监禁内存,就是这次照应式重构缩小56%内存占用的关键要素。
其余优化
有了双向链表后依赖触发也变得愈加明晰了,当某个照应式变量扭转后,只要要遍历Dep依赖(纵向)的Link节点组成的双向链表,而后经过这些Link节点间接访问到对应的Sub订阅者,触发其依赖。
基于此Sub订阅者的触发就是一个线性的环节,所以就可以成功将须要触发的Sub订阅者串起来组成了一个Sub订阅者组成的队列。等须要触发的订阅者搜集完了后,再去启动触发Sub订阅者,防止同一个订阅者被触发屡次。
依赖触发相比之前也变得愈加便捷了,性能以及内存也有所优化。
最后就是由于有了双向链表和版本计数的加持后,computed计算属性变得愈加痴呆,如今是惰性计算了。computed计算属性只要等有人经常使用他(比如在template中经常使用计算属性doubleCount)后才会去口头计算属性中的回调函数,以及3.4版本中就曾经成功的假设计算属性值没有变动,另外一个watch又监听了这个computed的值,此时这个watch不会被触发。关于这个可以看我之前的版本计数文章。
总结
Vue3.5照应式重构关键是经过双向链表和版本计数成功的,优化后内存占用缩小了56%。关键要素是:在新的照应式系统中多了一个Link节点用于链接Sub订阅者和Dep依赖,更新Sub订阅者依赖只是启动指针的变换,并且还能够复用Link节点以及将不再经常使用的Link节点给孤立进去便于V8更快的将这个Link节点给回收。此外还有Sub订阅者的触发也变得愈加便捷,以及如今是computed计算属性是惰性计算了,这些优化雷同也优化了内存的经常使用。