上篇解析了 Proxy 的使用细节,这篇解析下 vue3 使用 Proxy 实现响应式的各种细节。
什么是响应式
响应性是一种可以使我们声明式地处理变化的编程范式,最典型例子是 execl 公式,公式相关列输入值,公式会自动计算出结果。而在 vue 中就是自动更新视图。vue 采用 Proxy 对对象进行拦截,在 get 中收集依赖,在 set 中触发更新。这种方式当组件状态改变时,最会更新渲染当前组件。
vue 响应式的整体分为:依赖收集、派发更新、调度器三个模块。
属性读取拦截进行依赖收集
我们先来看看一个很简单的响应式示例:
其中副作用函数 effect 添加到 dep 中就是依赖收集的过程,我们将其放入 track 函数中。
我们将其进行优化下,把 Reflect 放入其中。
上面例子在依赖收集时没有对访问的对象的 key 进行区分,因此在 set 执行派发更新时会把 dep 中所有的依赖都执行了。因此我们要进行区分。
这里我们采用 Map 结构将依赖和对象的 key 对应起来,在派发更新时就能找到对应的依赖执行。
依赖收集的结构改了,那么派发更新也自然要修改。
依赖收集和派发更新都实现了,那么响应式怎么触发组件更新的呢?
当组件挂载时会声明组件更新的方法 componentUpdateFn 并将这个函数赋值给 activeEffect。
这样组件更新函数就被收集了。以上代码只是简单说明组件更新函数如何被收集,并不是真实代码。
in和for in的拦截
响应式拦截对象除了读和写操作外,还要拦截判断是否有属性操作 in
和遍历操作 for in
。
has
代理拦截 in 操作是通过 has 来进行拦截。
OwnKey
遍历操作 for in 需要通过 OwnKey 进行拦截,这里没有对应的 key,因此设置了 ITERATE_KEY 为 key,在对象属性有增减时触发该依赖。
当对象的添加或删除属性的时候触发,添加属性会触发 set 监听。因此在 set 中识别是 set 还是 add。
如果是 add,在触发依赖是加上 ITERATE_KEY 依赖。
完整代码
删除属性 delete 操作也会触发。
删除操作是通过 deleteProperty 来进行拦截。
set 触发响应
派发更新中可以对比新旧值,值改变时触发依赖。
但新旧值对比有个例外就是 NaN,因为 NaN 自身是不相等的,所以要专门处理。
深响应
vue3 实现深响应是在 get 方法中判断是否是深响应,是的话就其值代理。
但这里有个问题,深响应每次读取属性值是对象的都会返回一个新的代理对象,通过 proxyMap 来缓存代理对象来解决这个问题。
性能
其实 Proxy 的性能和 defineProperty 比并不是很好,我们用 benchmark 来简单测试下。
测试结果:
defineProperty 反而是最快的(ops/sec
值越大性能越好),这里考虑可能是 defineProperty 直接返回1,我们再修改代码再测试一次。
测试结果:
改完之后 defineProperty 仍然比原生的快一些,而 proxy 却慢很多。
我们再看看 set 触发情况测试
测试结果一样 proxy 要慢很多。
既然 Proxy 性能要比 defineProperty 慢很多,那为什么 vue3 还用 Proxy 替换 defineProperty 并且说性能更好呢?
相对 vue3 的响应式,触发 get/set 只是其中一部分性能,还有响应式初始化的性能,我们来看看初始化体性能如何。
可以看出初始化上 Poxy 的性能要比 defineProperty 好很多,使得组件的初始渲染要快很多。这就是 vue3 使用 Proxy 的主要性能提升,但后期触发响应时性能会差一些,特别是在数组上的触发响应。