Proxy 在 vue 中的使用细节

616 阅读4分钟

上篇解析了 Proxy 的使用细节,这篇解析下 vue3 使用 Proxy 实现响应式的各种细节。

什么是响应式

响应性是一种可以使我们声明式地处理变化的编程范式,最典型例子是 execl 公式,公式相关列输入值,公式会自动计算出结果。而在 vue 中就是自动更新视图。vue 采用 Proxy 对对象进行拦截,在 get 中收集依赖,在 set 中触发更新。这种方式当组件状态改变时,最会更新渲染当前组件。

vue 响应式的整体分为:依赖收集、派发更新、调度器三个模块。

属性读取拦截进行依赖收集

我们先来看看一个很简单的响应式示例:

code9.png

其中副作用函数 effect 添加到 dep 中就是依赖收集的过程,我们将其放入 track 函数中。

code10.png

我们将其进行优化下,把 Reflect 放入其中。

code11.png

上面例子在依赖收集时没有对访问的对象的 key 进行区分,因此在 set 执行派发更新时会把 dep 中所有的依赖都执行了。因此我们要进行区分。

code12.png

这里我们采用 Map 结构将依赖和对象的 key 对应起来,在派发更新时就能找到对应的依赖执行。

依赖收集的结构改了,那么派发更新也自然要修改。

code13.png

依赖收集和派发更新都实现了,那么响应式怎么触发组件更新的呢?

当组件挂载时会声明组件更新的方法 componentUpdateFn 并将这个函数赋值给 activeEffect。

code14.png

这样组件更新函数就被收集了。以上代码只是简单说明组件更新函数如何被收集,并不是真实代码。

in和for in的拦截

响应式拦截对象除了读和写操作外,还要拦截判断是否有属性操作 in 和遍历操作 for in

has

代理拦截 in 操作是通过 has 来进行拦截。

code1.png

OwnKey

遍历操作 for in 需要通过 OwnKey 进行拦截,这里没有对应的 key,因此设置了 ITERATE_KEY 为 key,在对象属性有增减时触发该依赖。

当对象的添加或删除属性的时候触发,添加属性会触发 set 监听。因此在 set 中识别是 set 还是 add。

code3.png

如果是 add,在触发依赖是加上 ITERATE_KEY 依赖。

code2.png

完整代码

code4.png

删除属性 delete 操作也会触发。

删除操作是通过 deleteProperty 来进行拦截。

code5.png

set 触发响应

派发更新中可以对比新旧值,值改变时触发依赖。

code6.png

但新旧值对比有个例外就是 NaN,因为 NaN 自身是不相等的,所以要专门处理。

code7.png

深响应

vue3 实现深响应是在 get 方法中判断是否是深响应,是的话就其值代理。

code8.png

但这里有个问题,深响应每次读取属性值是对象的都会返回一个新的代理对象,通过 proxyMap 来缓存代理对象来解决这个问题。

code23.png

性能

其实 Proxy 的性能和 defineProperty 比并不是很好,我们用 benchmark 来简单测试下。

code15.png

测试结果:

code16.png

defineProperty 反而是最快的(ops/sec值越大性能越好),这里考虑可能是 defineProperty 直接返回1,我们再修改代码再测试一次。

code17.png

测试结果:

code18.png

改完之后 defineProperty 仍然比原生的快一些,而 proxy 却慢很多。

我们再看看 set 触发情况测试

code19.png code20.png

测试结果一样 proxy 要慢很多。

既然 Proxy 性能要比 defineProperty 慢很多,那为什么 vue3 还用 Proxy 替换 defineProperty 并且说性能更好呢?

相对 vue3 的响应式,触发 get/set 只是其中一部分性能,还有响应式初始化的性能,我们来看看初始化体性能如何。

code21.png code22.png

可以看出初始化上 Poxy 的性能要比 defineProperty 好很多,使得组件的初始渲染要快很多。这就是 vue3 使用 Proxy 的主要性能提升,但后期触发响应时性能会差一些,特别是在数组上的触发响应。