Vue Reactivity 响应式库
目录
@vue/reactivity
(下称 VueReactivity) 是 Vue3 内部实现响应式编程的包。它和很多响应式编程的库一样,都提供类似的 API 和实现原理。
Vue 组件中的 data 从组件挂载开始,就通过这个库赋予了响应式结构,之后的 set 操作都会触发组件渲染动作。
实现原理
VueReactivity 和很多响应式库都有相似的实现原理:依赖追踪 + Proxy。
- 依赖追踪是隐式地找出观察者需要的依赖,并在依赖变化时通知观察者。
- Proxy 是数据代理 API,可以给目标对象 get、set 等操作装饰一些额外的功能。
依赖追踪
依赖追踪在单线程下的 JavaScript 很容易实现。单线程可以保证某一时刻只能操作一个对象,那么在出现对象操作时,可以把当前所处的上下文记下来,当上下文结束时,就可以知道这个上下文访问了哪些对象,并观察它们。
一般这个上下文指的是某个函数。而操作目标对象时,需要把这个对象加入到当前的依赖列表。
let dependencies = []
function trackDeps(fn: Function) {
dependencies = []
fn()
return Array.from(dependencies)
}
// user-code
let person = { name: 'luo' }
const trackedDeps = trackDeps(() => {
dependencies.push(person)
console.log('person name is ' + String(person.name))
})
// [person]
Proxy
注意上方的例子 user-code
处,可以看到这个函数必须调用 dependencies.push(person)
才能把依赖记下来。而 Proxy 可以为常规的对象操作施加额外的操作,这些操作对用户并不可见。这样就可以实现用户在不需要感知的情况下,记下需要的依赖。
let dependencies = []
function trackDeps(fn: Function) {
dependencies = []
fn()
return Array.from(dependencies)
}
function makeProxy(target) {
return new Proxy(target, {
get: (obj, key) => {
// 装饰额外的操作
dependencies.push(target)
// 原本的读 name 操作
return Reflect.get(obj, key)
}
})
}
// user-code
let person = makeProxy({ name: 'luo' })
const trackedDeps = trackDeps(() => {
// 用户定义函数中,不再需要显式通知依赖
console.log('person name is ' + String(person.name))
})
// [person]
在具体的实现中还需要处理缓存、数组等更为复杂的情况。这里只描述核心原理。
概念 & Snippet
Ref
最细粒度的响应式对象,可以用来给原始类型装箱拆箱。
let num = ref(1)
unref(num)
// 1
num.value
// 1
num.value = 2
// 2
Reactive
只能包装对象的响应式对象,只能为普通对象、Array、Map、Set、WeakMap、WeakSet。
let person = reactive({ name: 'luo' })
person.name
// luo
person.name = 'al1ce'
// al1ce
Effect
副作用是对依赖有反应的观察者对象,带有一个回调函数,函数执行后会产生依赖,依赖变化时反过来触发回调。会对任何响应式对象有反应。
let dep1 = reactive({ name: 'appbar' })
let dep2 = reactive({ age: 17 })
let dep3 = reactive(['react', 'vue', 'vite', 'mobx'])
effect1 = effect(() => {
console.log('dep1 name: ' + dep1.name)
})
effect2 = effect(() => {
console.log('dep2 age: ' + dep2.age)
})
effect3 = effect(() => {
console.log('dep3 skills: ' + dep3.join(','))
})
// 对象 setter 触发副作用回调
dep1.name = 'apptitle'
dep2.age = 17.9
dep3.push('rollup')
// 取消观察者
stop(effect1)
stop(effect2)
stop(effect3)
Computed
特殊的副作用,它对依赖变化产生反应的同时,自己本身也可以是一个依赖。
let timer = {
second: ref(0),
handle: -1,
start: () => timer.handle = setInterval(() => timer.second.value++, 1000),
stop: () => clearInterval(timer.handle),
}
let formatted = computed(() => String(timer.second.value) + ' second')
let printEffect = effect(() => console.log(formatted.value))
// 开始,10秒后取消任务
timer.start()
effect(() => {
if (timer.second.value == 10) {
timer.stop()
stop(printEffect)
}
})
附录
VueReactivity 可以脱离 Vue 应用单独使用。