您现在的位置是:网站首页> 编程资料编程资料

Vue.js3.2响应式部分的优化升级详解_vue.js_

2023-05-24 349人已围观

简介 Vue.js3.2响应式部分的优化升级详解_vue.js_

背景

Vue 3 正式发布距今已经快一年了,相信很多小伙伴已经在生产环境用上了 Vue 3 了。如今,Vue.js 3.2 已经正式发布,而这次 minor 版本的升级主要体现在源码层级的优化,对于用户的使用层面来说其实变化并不大。其中一个吸引我的点是提升了响应式的性能:

More efficient ref implementation (~260% faster read / ~50% faster write)

~40% faster dependency tracking

~17% less memory usage

翻译过来就是 ref API 的读效率提升约为 260%,写效率提升约为 50% ,依赖收集的效率提升约为 40%,同时还减少了约 17% 的内存使用。

这简直就是一个吊炸天的优化啊,因为要知道响应式系统是 Vue.js 的核心实现之一,对它的优化就意味着对所有使用 Vue.js 开发的 App 的性能优化。

而且这个优化并不是 Vue 官方人员实现的,而是社区一位大佬 @basvanmeurs 提出的,相关的优化代码在 2020 年 10 月 9 号就已经提交了,但由于对内部的实现改动较大,官方一直等到了 Vue.js 3.2 发布,才把代码合入。

这次 basvanmeurs 提出的响应式性能优化真的让尤大喜出望外,不仅仅是大大提升了 Vue 3 的运行时性能,还因为这么核心的代码能来自社区的贡献,这就意味着 Vue 3 受到越来越多的人关注;一些能力强的开发人员参与到核心代码的贡献,可以让 Vue 3 走的更远更好。

我们知道,相比于 Vue 2,Vue 3 做了多方面的优化,其中一部分是数据响应式的实现由 Object.defineProperty API 改成了 Proxy API。

当初 Vue 3 在宣传的时候,官方宣称在响应式的实现性能上做了优化,那么优化体现在哪些方面呢?有部分小伙伴认为是 Proxy API 的性能要优于 Object.defineProperty 的,其实不然,实际上 Proxy 在性能上是要比 Object.defineProperty 差的,详情可以参考 Thoughts on ES6 Proxies Performance 这篇文章,而我也对此做了测试,结论同上,可以参考这个 repo

既然 Proxy 慢,为啥 Vue 3 还是选择了它来实现数据响应式呢?因为 Proxy 本质上是对某个对象的劫持,这样它不仅仅可以监听对象某个属性值的变化,还可以监听对象属性的新增和删除;而 Object.defineProperty 是给对象的某个已存在的属性添加对应的 gettersetter,所以它只能监听这个属性值的变化,而不能去监听对象属性的新增和删除。

而响应式在性能方面的优化其实是体现在把嵌套层级较深的对象变成响应式的场景。在 Vue 2 的实现中,在组件初始化阶段把数据变成响应式时,遇到子属性仍然是对象的情况,会递归执行 Object.defineProperty 定义子对象的响应式;而在 Vue 3 的实现中,只有在对象属性被访问的时候才会判断子属性的类型来决定要不要递归执行 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有一定的提升。

因此,相比于 Vue 2,Vue 3 确实在响应式实现部分做了一定的优化,但实际上效果是有限的。而 Vue.js 3.2 这次在响应式性能方面的优化,是真的做到了质的飞跃,接下来我们就来上点硬菜,从源码层面分析具体做了哪些优化,以及这些优化背后带来的技术层面的思考。

响应式实现原理

所谓响应式,就是当我们修改数据后,可以自动做某些事情;对应到组件的渲染,就是修改数据后,能自动触发组件的重新渲染。

Vue 3 实现响应式,本质上是通过 Proxy API 劫持了数据对象的读写,当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知。

接下来,我们简单分析一下依赖收集和派发通知的实现(Vue.js 3.2 之前的版本)。

依赖收集

首先来看依赖收集的过程,核心就是在访问响应式数据的时候,触发 getter 函数,进而执行 track 函数收集依赖:

let shouldTrack = true // 当前激活的 effect let activeEffect // 原始数据对象 map const targetMap = new WeakMap() function track(target, type, key) { if (!shouldTrack || activeEffect === undefined) { return } let depsMap = targetMap.get(target) if (!depsMap) { // 每个 target 对应一个 depsMap targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { // 每个 key 对应一个 dep 集合 depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { // 收集当前激活的 effect 作为依赖 dep.add(activeEffect) // 当前激活的 effect 收集 dep 集合作为依赖 activeEffect.deps.push(dep) } } 

分析这个函数的实现前,我们先想一下要收集的依赖是什么,我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。

track 函数拥有三个参数,其中 target 表示原始数据;type 表示这次依赖收集的类型;key 表示访问的属性。

track 函数外部创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 targetkey,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。为了方便理解,可以通过下图表示它们之间的关系:

因此每次执行 track 函数,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。

派发通知

派发通知发生在数据更新的阶段,核心就是在修改响应式数据时,触发 setter 函数,进而执行 trigger 函数派发通知:

const targetMap = new WeakMap() function trigger(target, type, key) { // 通过 targetMap 拿到 target 对应的依赖集合 const depsMap = targetMap.get(target) if (!depsMap) { // 没有依赖,直接返回 return } // 创建运行的 effects 集合 const effects = new Set() // 添加 effects 的函数 const add = (effectsToAdd) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { effects.add(effect) }) } } // SET | ADD | DELETE 操作之一,添加对应的 effects if (key !== void 0) { add(depsMap.get(key)) } const run = (effect) => { // 调度执行 if (effect.options.scheduler) { effect.options.scheduler(effect) } else { // 直接运行 effect() } } // 遍历执行 effects effects.forEach(run) } 

trigger 函数拥有三个参数,其中 target 表示目标原始对象;type 表示更新的类型;key 表示要修改的属性。

trigger 函数 主要做了四件事情:

  • targetMap 中拿到 target 对应的依赖集合 depsMap
  • 创建运行的 effects 集合;
  • 根据 keydepsMap 中找到对应的 effect 添加到 effects 集合;
  • 遍历 effects 执行相关的副作用函数。

因此每次执行 trigger 函数,就是根据 targetkey,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。

在描述依赖收集和派发通知的过程中,我们都提到了一个词:副作用函数,依赖收集过程中我们把 activeEffect(当前激活副作用函数)作为依赖收集,它又是什么?接下来我们来看一下副作用函数的庐山真面目。

副作用函数

那么,什么是副作用函数,在介绍它之前,我们先回顾一下响应式的原始需求,即我们修改了数据就能自动做某些事情,举个简单的例子:

import { reactive } from 'vue' const counter = reactive({ num: 0 }) function logCount() { console.log(counter.num) } function count() { counter.num++ } logCount() count() 

我们定义了响应式对象 counter,然后在 logCount 中访问了 counter.num,我们希望在执行 count 函数修改 counter.num 值的时候,能自动执行 logCount 函数。

按我们之前对依赖收集过程的分析,如果logCountactiveEffect 的话,那么就可以实现需求,但显然是做不到的,因为代码在执行到 console.log(counter.num) 这一行的时候,它对自己在 logCount 函数中的运行是一无所知的。

那么该怎么办呢?其实只要我们运行 logCount 函数前,把 logCount 赋值给 activeEffect 就好了:

activeEffect = logCount logCount() 

顺着这个思路,我们可以利用高阶函数的思想,对 logCount 做一层封装:

function wrapper(fn) { const wrapped = function(...args) { activeEffect = fn fn(...args) } return wrapped } const wrappedLog = wrapper(logCount) wrappedLog() 

wrapper 本身也是一个函数,它接受 fn 作为参数,返回一个新的函数 wrapped,然后维护一个全局变量 activeEffect,当 wrapped 执行的时候,把 activeEffect 设置为 fn,然后执行 fn 即可。

这样当我们执行 wrappedLog 后,再去修改 counter.num,就会自动执行 logCount 函数了。

实际上 Vue 3 就是采用类似的做法,在它内部就有一个 effect 副作用函数,我们来看一下它的实现:

// 全局 effect 栈 const effectStack = [] // 当前激活的 effect let activeEffect function effect(fn, options = EMPTY_OBJ) { if (isEffect(fn)) { // 如果 fn 已经是一个 effect 函数了,则指向原始函数 fn = fn.raw } // 创建一个 wrapper,它是一个响应式的副作用的函数 const effect = createReactiveEffect(fn, options) if (!options.lazy) { // lazy 配置,计算属性会用到,非 lazy 则直接执行一次 effect() } return effect } function createReactiveEffect(fn, options) { const effect = function reactiveEffect() { if (!effect.active) { // 非激活状态,则判断如果非调度执行,则直接执行原始函数。 return options.scheduler ? undefined : fn() } if (!effectStack.includes(effect)) { // 清空 effect 引用的依赖 cleanup(effect) try { // 开启全局 shouldTrack,允许依赖收集 enableTracking() // 压栈 effectStack.push(effect) activeEffect = effect // 执行原始函数 return fn() } finally { // 出栈 effectStack.pop() // 恢复 shouldTrack 开启之前的状态 resetTracking() // 指向栈最后一个 effect activeEffect = effectStack[effectStack.length - 1] } } } effect.id = uid++ // 标识是一个 effect 函数 effect._isEffect = true // effect 自身的状态 effect.active = true // 包装的原始函数 effect.raw = fn // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用 effect.deps = [] // effect 的相关配置 effect.options = options return effect } 

结合上述代码来看,effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数,为了和外部的 effect 函数区分,我们把它称作 reactiveEffect 函数,并且还给它添加了一些额外属性(我在注释中都有标明)。另外,effect 函数还支持传入一个配置参数以支持更多的 feature,这里就不展开了。

reactiveEffect 函数就是响应式的副作用函数,当执行 trigger 过程派发通知的时候,执行的 effect 就是它。

按我们之前的分析,reactiveEffect 函数只需要做两件事情:让全局的 activeEffect 指向它, 然后执行被包装的原始函数 fn

但实际上它的实现要更复杂一些,首先它会判断 effect 的状态是否是 active,这其实是一种控制手段,允许在非 active 状态且非调度执行情况,则直接执行原始函数 fn 并返回。

接着判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。之前我们提

-六神源码网