Ng老狗的Vue 3 响应式系统笔记
响应式库 @vue/reactivity 学习记录
背景
作为一个多年Angular应用的开发者,提到响应式,第一反应就是Rxjs。
相比于Promise的命令式编程分散的信号源, Rxjs的声明式响应数据流,在 用户事件多样且频繁的组件 中,能更优雅地处理异步数据流,往往 一个Observable就能完整的体现整个数据流 。
初见Vue 3的响应式,是一种既熟悉有新鲜的感觉。
我不再需要各种各样的Observable, Subject, BehaviorSubject 来表达数据流, 而是通过简单的ref和reactive就能创建响应式数据; 我也不需要去订阅和取消订阅数据流, Vue 3的响应式系统会 自动追踪依赖,并在数据变化时更新。
响应式亦有不同
诚然,Rxjs是一个强大的响应式编程库,适用于复杂的异步数据流处理, 而Vue 3的响应式系统更专注于UI层的数据绑定和状态管理, 在前端开发中,提供了极其便捷和高效的响应式编程体验。
Ps: Angular 16也引入了类似Vue 3的响应式系统,称为Signals。
基础概念
ref 和 reactive 对比 Observable
reactivity中最重要的无疑是 ref 和 reactive, 他们类似 Rxjs 中的 BehaviorSubject, 用于收集下游事件,并在更新时 广播 订阅者。
而 effect, computed 则完成了响应式数据流的蜕变, 自动的依赖收集和触发,彻底摆脱了手动订阅和取消订阅的繁琐。
以自动加法,举一个栗子:
typescript
/************* @vue/reactivity ***************/
import { ref, reactive, computed, effect } from '@vue/reactivity';
const a = ref(1);
const b = ref(2);
// 响应式数据流
const sum = computed(() => a.value + b.value);
// 触发
a.value = 3;
console.log(sum.value); // 3 + 2 = 5
/************* Rxjs **************************/
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
const a$ = new BehaviorSubject(1);
const b$ = new BehaviorSubject(2);
const sum$ = combineLatest([a$, b$]).pipe(
map(([a, b]) => a + b)
);
// 触发
a$.next(3);
// 特别的数据流只有在订阅时, 才会触发上游计算
sum$.subscribe(console.log); // 3 + 2 = 5
源码阅读
响应式系统的源码并不复杂,核心部分大约几百行代码。 主要包括以下几个模块:
- 响应式数据创建:
ref和reactive函数 - 依赖收集和触发:
effect函数 - 计算属性:
computed函数 - 响应式数据的代理和拦截:
Proxy对象 - 响应式数据的只读和深度响应式:
readonly和shallowReactive函数 ...等等
其中,依赖收集和触发是响应式系统的核心
一、依赖收集
- 看懂
effect,就基本了解依赖收集的核心原理
typescript
// 例如:
import { effect, ref } from '@vue/reactivity';
const count = ref(0);
effect(() => {
console.log(`count is: ${count.value}`);
});
在这里如果把 .value 不要看作字段的取值,而是看成 .subscribe(currentEffect) 方法的调用,整个事情就变得很清晰了。
我只需要在 effect 的函数中执行时, 添加一个 currentEffect的全局变量 ,作为上下文, 每次 .subscribe() 的时候,把这个上下文添加到依赖列表中, 就很方便地完成了依赖收集。
computed 和 watch 也是类似的原理。
二、依赖触发
由于值更新的场景更灵活,依赖触发的设计也就更加复杂些。
Vue中按照 基本数据类型 和 对象类型 分别处理。参考GitHub
- 基本数据类型:直接在
.value的setter中触发依赖 参考RefImpl - 对象类型:通过
Proxy的set拦截器触发依赖参考MutableReactiveHandler。嵌套的对象会在get时递归地转换为响应式对象。参考BaseReactiveHandler
当然,非必要的场景下,Vue 也提供了 shallowReactive 和 shallowRef 来创建浅响应式对象。
三、计算属性
计算属性 computed 的实现也很巧妙。 它本质上是一个带有缓存的 effect。 当计算属性的依赖发生变化时,计算属性会重新计算值, 但只有在访问计算属性的值时才会触发重新计算。
这点和Rxjs中的 shareReplay 操作符有些类似。
四、总结
通过阅读 Vue 3 响应式系统的源码, 对响应式编程的有了新的理解。
核心的依赖清单通过数组管理、 相似的触发逻辑 和响应式数据流的理念,等等保留了发布订阅的稳定设计。
Vue 3 的响应式库,不拘泥于刻板的设计模式, 而是根据实际需求,灵活地设计了响应式数据的创建、依赖收集和触发机制, 从而实现了高效且易用的响应式编程体验。
Vue团队中Anthony Fu在这篇文章中对设计理念有更清晰的阐述,推荐一读。