1. 调度执行
可调度性是响应式系统非常重要的一个特性,所谓可调度性就是当trigger触发时 控制副作用函数的执行时机和执行次数的能力。比如连续更改data.a时,如果按照之前写的逻辑就会发现data.a改变了几次,副作用函数就触发了几次,这显然不是我们想要的结果。
data.a++
data.a++
data.a++
我们改进一下effect方法,多接收一个option对象,并挂在efectFn上方便拓展一些能力。
const effect = (fn, options = {}) => {
const effectFn = () => {
effectFn.deps.forEach(bucket => bucket.delete(effectFn));
effectFn.deps = []
effectStack.push(effectFn)
activeFuction = effectFn
const res = fn()
effectStack.pop()
activeFuction = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
effectFn.options = options
effectFn()
}
调用efeect方法传入option,option中有一个scheduler函数,然后在trigger方法内判断当前依赖是否有scheduler,有就执行scheduler并把当前依赖传入,否则执行依赖。
effect(() => {
console.log(data.a)
}, {
scheduler(fn) {
fn()
}
})
const trigger = (target, key) => {
let targetMap = bucketMap.get(target)
if (!targetMap) return
let deps = targetMap.get(key)
if (!deps) return
new Set(deps).forEach(f => {
if (f !== activeFuction) {
f.options?. scheduler? f.options.scheduler(f) : f()
}
});
}
这么写就能做到 连续改变data.a 只触发一次依赖吗?当然不能,目前只是套了一层scheduler,但最终还是直接执行了依赖。我们需要在scheduler中,把所有的依赖都添加到一个队列里,然后一次性触发。
定义全局变量jobQueue = new Set(),在scheduler中将fn压入jobQueue中,然后flushQueue,flushQueue就是在Promise.then中遍历jobQueue执行其中的方法。在Promise.then中遍历jobQueue是为了让jobQueue充分一次事件循环中的所有依赖统一执行,最后将jobQueue给clear掉。
let jobQueue = new Set()
let p = Promise.resolve()
let isFlushing = false
const flushQueue = () => {
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach((fn) => fn())
}).finally(() => {
isFlushing = false
jobQueue.clear()
})
}
effect(() => {
console.log(data.a)
}, {
scheduler(fn) {
jobQueue.add(fn)
flushQueue()
}
})
2. lazy与computed
在目前的代码基础上,调用effect传入的fn会立即执行一次,有时我不希望将传入的fn立即执行,而是在使用的时候才去执行,可以在options中加个lazy属性,然后在effect中做点手脚,以达到“懒执行”的期望。
const effect = (fn, options = {}) => {
const effectFn = () => {
effectFn.deps.forEach(bucket => bucket.delete(effectFn));
effectFn.deps = []
effectStack.push(effectFn)
activeFuction = effectFn
const res = fn()
effectStack.pop()
activeFuction = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn()
}
return effectFn
}
const test = effect(() => {
return data.a + data.b
}, {
lazy: true,
scheduler(fn) {
jobQueue.add(fn)
flushQueue()
}
})
let val = test()
effect最终返回一个函数,可以手动去执行它,其实单纯手动执行意义并不大,但可以通过执行它来得到fn的返回值。
基于目前的代码,定义一个computed函数,接收一个getter函数,在函数内部声明effectRes等于effect返回的函数,执行effectRes的结果就等于副作用函数的返回值了,定义obj对象,obj.value是个访问属性,只有当读取value的值时,才会执行getter并将其结果作为返回值返回。
const computed = (getter) => {
const effectRes = effect(getter, {
lazy: true,
scheduler(fn) {
jobQueue.add(fn)
flushQueue()
}
})
const obj = {
get value() {
return effectRes()
}
}
return obj
}
let res = computed(() => {
console.log('computed')
return data.a + data.b
})
console.log(res.value)
除了data.a 和 data.b的变动和访问res.value 的时候都会重新计算一遍data.a + data.b,其实懒加载和computed就已经成型了。
3. 计算属性的缓存
那么data.a 和 data.b的变动一定会触发副作用函数这个没有问题,但当data.a和data.b不变的时候访问res.value不需要每次都重新计算吧,也就是说现在需要做计算所得值的缓存。
const computed = (getter) => {
let value = null
let dirty = true
const effectRes = effect(getter, {
lazy: true,
scheduler(fn) {
dirty = true
jobQueue.add(fn)
flushQueue()
}
})
const obj = {
get value() {
if (dirty) {
value = effectRes()
dirty = false
}
return value
}
}
return obj
}
console.log(res.value)
data.a++
console.log(res.value)
在computed函数内声明value来做计算所得值的缓存,dirty来标志value是否需要重新计算,函数首次调用和data.a或data.b改变的时候需要将dirty置为true,访问res.value时判断dirty如果是true则重新计算获得结果赋值给value,最后返回value的值,这样就能完成计算属性的缓存了。
当我们执行以下这段代码的时候,data.a++后会重新打印res.value吗?
effect(() => {
console.log('effect:', res.value)
})
data.a++
答案是不行,原因很简单res并不是一个Proxy对象,没有对它进行数据拦截。那就需要在computed中手动的去调用track和tigger函数从而拦截value属性加入响应式的逻辑。
const computed = (getter) => {
let value = null
let dirty = true
const effectRes = effect(getter, {
lazy: true,
scheduler(fn) {
dirty = true
jobQueue.add(fn)
flushQueue()
trigger(obj, 'value')
}
})
const obj = {
get value() {
if (dirty) {
value = effectRes()
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}