Vue的响应式原理已经有很多人说过了,读取时响应式数据会收集对应的副作用函数,设置响应式数据时触发副作用函数执行。这是最基本的Vue响应式工作原理,但是Vue借助响应系统实现了许多API,如watch(),watchEffect(),computed(),ref()和reactive()等。然后我就通过几篇文章介绍一下这几个API的实现原理。
在开始之前先简单实现一个effect函数,可以用它来注册副作用函数:
let activateEffect
function effect(fn, options = {}) {
const effectFn = () => {
activateEffect = effectFn
// 返回fn的结果
const res = fn()
return res
}
// 将options添加到effectFn上
effectFn.options = options
// 保存所有与该副作用函数相关的依赖
effectFn.deps = []
// 如果option中不指定懒执行
if (!options.lazy) {
effectFn()
}
return effectFn
}
effect接受两个参数,effect的回调和options,如果options指定了懒执行那么就只返回包装函数effectFn,effectFn返回回调的执行结果res。
然后实现收集副作用函数的track:
// 使用weakMap来保存每个target对应的依赖映射,target回收后对应的依赖映射也一并回收
let bucket = new WeakMap()
// 追踪依赖
function track(target, key) {
if (!activateEffect) return
let depsMap = bucket.get(target)
//找不到就新建一个,注意是使用的Map
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
//找不到就新建一个
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 收集全局唯一的activeEffect
deps.add(activateEffect)
// 让每个副作用函数都能知道时谁收集了自己,用于解除跟踪
activateEffect.deps.push(deps)
}
这个函数用来在读取响应式数据时收集activeEffect。
和触发副作用函数重新执行的trigger:
// 触发执行
function trigger(target, key) {
// 寻找target
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(target)
const effectToRun = new Set()
// 如果注册activeEffect的过程中,改变了响应式数据,会导致又track又trigger的过程,引发无线递归。所以只用去掉activateEffect的副作用函数
effects &&
effects.forEach((fn) => {
if (fn !== activateEffect) {
effectToRun.add(fn)
}
})
effectToRun.forEach((fn) => {
// 如果注册副作用函数的过程中添加了scheduler,那么把fn作为参数调用scheduler
if (fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
trigger在设置响应式数据时触发重新执行,但是如果副作用函数设置了scheduler时,那么以依赖为参数触发sheduler。
还有一点值得注意就是,如果track的过程中改变了响应式数据,会导致trigger执行,但是这个副作用函数现在正在首次执行中(所以他也是activeEffect),所以导致了递归,解决办法就是触发副作用函数时去掉activeEffect。
Computed:
先来看看computed(),computed这个API接受一个响应式数据的getter,computed的返回值在getter依赖的响应式数据变更后返回新的值:
computed还有一个特点是只有在读取它的返回值的时候,这个getter才会执行,所以可以推断出它是一个懒执行的effect:
// options中指定lazy时会直接返回包装函数effectFn,执行它可以得到getter的结果
const effectFn = effect(getter, { lazy:true })
因为我们知道computed返回值也是一个key为value的getter,所以可以给出computed的简单实现:
function Computed(getter) {
const effectFn = effect(getter, { lazy:true })
const obj = {
get value() {
return effect()
}
}
return obj
}
这样就实现了通过computed的返回值obj的obj.value访问我们想要的getter的返回值,但是这么做还不够,一是因为getter是有缓存机制的,而且computed的返回值也是响应式,会根据getter响应式数据的变化而变化。
为了实现缓存机制,需要在getter中的响应式数据不变时,只读取缓存,在getter中的响应式数据变化时重新计算。另外还需要实现computed返回值的响应式,读取obj.value的时候收集副作用函数,getter的数据更改的时候,触发副作用函数:
function Computed(getter) {
let value
// 用来判断是否需要重新计算
let _dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
// 如果setter的数据改变了,会触发scheduler,_dirty更新为true,代表需要重新计算
// 同时手动触发trigger,让依赖于computed返回值的副作用函数重新执行得到新值
_dirty = true
// trigger时会重新读取obj.value,拿到新值
trigger(obj, 'value')
}
})
const obj = {
get value() {
// 只有数据为“脏”时,才重新计算value
if (_dirty) {
// 通过执行effectFn()收集getter,也就相当于收集scheduler,
// 而scheduler又会触发computed返回值的依赖
value = effectFn()
_dirty = false
}
// 读取computed返回值时,收集对应的依赖
// 只要在副作用函数中读取过computed返回值,就会被收集,无论值是否为“脏”
track(obj, 'value')
return value
}
}
return obj
}
这样就实现了computed的三个功能,读取时执行getter拿到值、值的缓存、computed返回值的响应式。 如果值不为脏,那么读取时只会读取缓存,只有响应式数据变化了,才会获得新值。另外setter依赖的响应式数据的变化,不会执行setter本身,而是根据调度系统执行trigger,trigger中运行computed返回值的副作用函数,重新读取computed返回值,然后重新计算effectFn得到结果。实现响应式。
剩余API会在后篇讲解。
参考:《Vue.js设计与实现》