上两篇文章,分别总结了响应式系统的基础实现和细节问题处理。接下来还剩下两个重点实现需要总结,一是围绕computed相关的实现原理,二是围绕watch相关的实现。先讲computed是因为,watch实现有部分基于computed的实现。先看computed部分。
computed相关实现
调度器实现
所谓调度器,是指控制副作用函数如何执行的函数。比如,可以让副作用函数异步执行。通过effect函数中增加一个options对象参数,来传递scheduler调度器。
核心实现主要涉及两个地方的修改,一个是调度器函数在effect中的传参,一个是调度器函数在handler.setter中的执行。
修改代码如下:
function effect (fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
// 增加options
effectFn.options = options
effectFn()
}
function trigger(target, key) {
...
effectToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
完整代码如下:
const bucket = new WeakMap()
const data= { ok: true, text: 'hello world' }
function track(target, key) {
let depsMap
if (!bucket.get(target)) {
bucket.set(target, new Map())
}
depsMap = bucket.get(target)
let deps
if (!depsMap.get(key)) {
depsMap.set(key, new Set())
}
deps = depsMap.get(key)
if (activeEffect) {
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
}
function trigger(target, key) {
let depsMap
if (!bucket.get(target)) return
depsMap = bucket.get(target)
let effects
if (!depsMap.get(key)) return
effects = depsMap.get(key)
const effectToRun = new Set()
effects.forEach(effect => {
if (effect !== activeEffect) {
effectToRun.add(effect)
}
})
effectToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
trigger(target, key, newVal);
}
})
function cleanup (effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
let activeEffect
let effectStack = []
function effect (fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
// 增加options
effectFn.options = options
effectFn()
}
effect(() => {
console.log(obj.text)
}, {
scheduler: effect => setTimeout(effect)
})
经过改造,effect已经实现了调度器功能。这个调度器的实现,是后续很多computed功能实现的基础。
lazy实现
为了减少不必要的副作用执行,保证可以懒执行副作用函数,在实现computed之前,我们着手实现lazy。
lazy功能的修改主要在两个地方,一是effect函数,lazy的情况下,要返回一个副作用函数引用,而非执行副作用函数。二是为了lazy执行的副作用函数,可以直接获取getter的返回值(getter作为副作用函数),要在effectFn中把执行结果返回。
直接看代码如下:
function effect (fn, options = {}) {
const effectFn = () => {
let res
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res // 增加返回值
}
effectFn.deps = []
effectFn.options = options
if (!effectFn.options.lazy) {
effectFn()
}
return effectFn // lazy的情况下,返回effectFn,而非执行
}
经过这样的修改,支持lazy的effect函数就实现了。
看看完整代码:
const bucket = new WeakMap()
const data= { ok: true, text: 'hello world' }
function track(target, key) {
let depsMap
if (!bucket.get(target)) {
bucket.set(target, new Map())
}
depsMap = bucket.get(target)
let deps
if (!depsMap.get(key)) {
depsMap.set(key, new Set())
}
deps = depsMap.get(key)
if (activeEffect) {
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
}
function trigger(target, key) {
let depsMap
if (!bucket.get(target)) return
depsMap = bucket.get(target)
let effects
if (!depsMap.get(key)) return
effects = depsMap.get(key)
const effectToRun = new Set()
effects.forEach(effect => {
if (effect !== activeEffect) {
effectToRun.add(effect)
}
})
effectToRun.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
trigger(target, key, newVal);
}
})
function cleanup (effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
let activeEffect
let effectStack = []
function effect (fn, options = {}) {
const effectFn = () => {
let res
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
effectFn.options = options
if (!effectFn.options.lazy) {
effectFn()
}
return effectFn
}
computed基础实现
computed基础实现非常简单,就是通过一个lazy的effect函数,作为一个对象的value值,并返回这个对象。
function computed (getter) {
const effectFn = effect(getter, { lazy: true })
const obj = {
get value() {
return effectFn()
}
}
return obj
}
由于computed实现的代码非常清晰简单,且暂时不涉及其他部分代码,这里就不列出来。
computed增加缓存
为了给computed增加缓存,需要用一个dirty布尔值来标志是否需要重新计算。因此,dirty的变更时机就是缓存实现的重点。当计算了第一次的时候,dirty变更为false;当副作用函数被响应式数据通知变更的时候,dirty要恢复为true。dirty恢复为true的实现,就依赖最初实现的调度器。
这部分代码也比较独立,直接看computed代码:
function computed (getter) {
let value
let dirty = true
const effectFn = effect(getter,
{ lazy: true,
scheduler() { dirty = true } // dirty恢复为true
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false // 计算完之后,闭包缓存value,dirty为false
}
return value
}
}
return obj
}
computed的值不会触发依赖收集
最后解决computed不会触发依赖收集的问题。
问题代码如下:
const sumRes = computed(() => obj.text)
effect(() => {
console.log(sumRes.value)
})
当obj.text变更的时候,console.log(sumRes.value)并不会执行。原因也毕竟清晰,因为sumRes本质是一个没有被经过Proxy代理的对象,并没有被bucket收集。为了解决这个问题,可以手动触发依赖的收集和触发。
function computed (getter) {
let value
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, 'value') // 手动trigger
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value') // 手动track
return value
}
}
return obj
}
经过上面的修复,这个问题就解决了,computed的实现也完成。简单总结一下computed的实现过程,首先是调度器和lazy的实现作为铺垫,后面实现了computed和缓存的功能,最后解决了computed内部对象的依赖收集和触发的问题。