上一篇总结,讲到响应式系统的最基础实现。除了最基础实现之外,其实还有一些细节问题需要处理。第二部分,我会针对这些细节问题,进行总结。这些细节问题,说实话,如果不是作者书中讲解,我自己很难想到,我想,这个这就是学习的价值。
解决比较难想到的细节问题
分支切换问题
分支切换问题,我的理解是,当一个响应式属性为某个值的时候,副作用不再和另一个属性有关联。但是目前的实现,即使无关联,也会导致副作用函数触发。
这个阐述有点抽象,直接看代码例子最好。
effect(() => {
const result = obj.ok ? obj.text : 'not'
console.log(result)
})
obj.ok = false
obj.text = 'changed'
代码例子中,预期是希望obj.ok为false的时候,不再触发effect注册的副作用函数,因为默认都是返回not。
为了解决这个问题,核心是使用cleanup函数,在执行handler.set的时候,先解除所有的副作用和bucket的对应关系,然后触发依赖收集,建立新的对应关系。这样就可以保证分支切换的时候,不会用冗余的副作用函数触发。
要解除所有的副作用和bucket的对应关系,则需要知道effect注册的副作用函数,被哪些Set所收集,所以需要在effect注册的副作用函数上,增加一个deps属性,用于反向收集。
为了方便阅读,我还是会把完整的代码写出来。
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 deps
if (!depsMap.get(key)) return
deps = depsMap.get(key)
// 修复无限循环的问题
const effectToRun = new Set(deps)
effectToRun.forEach(effect => {
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
function effect (fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
effect(() => {
const result = obj.ok ? obj.text : 'not'
console.log(result)
})
这里简单说一个细节。
// 修复无限循环的问题
const effectToRun = new Set(deps)
effectToRun.forEach(effect => {
effect()
})
trigger函数中,如果没有用一个新的Set来维护需要执行的副作用,会陷入无限循环。核心原因是因为,Set中移除了一个副作用函数之后,又新增同一个副作用函数,会导致这个Set永远无法遍历结束。这个是Set的规范规定,想要深入可以检索一下资料。
effect嵌套问题
由于渲染函数是使用effect,而渲染函数可能会有嵌套,本质上来说也就是组件的嵌套。那么effect的注册函数,也是要支持嵌套。现在的实现,如果嵌套effect,会出现内层effect影响外层effct的问题。
原因也比较简单,因为只有一个activeEffect变量去存储当前的副作用函数。当effect发生嵌套,且内层effect先触发依赖收集,那么activeEffect是内层effect注册的副作用,而到了外层的时候,仍然收集了这个内层副作用依赖。
能还原问题的代码如下:
effect(() => {
effect(() => {
console.log(obj.text)
})
console.log(obj.ok)
})
obj.ok = false
// 打印
// hello world
// true
// hello world <= 这里应该打印多一行,hello world
为了解决这个问题,熟悉数据结构的话,很容易想到要用栈来解决。
核心修改如下
let activeEffect
let effectStack = []
function effect (fn) {
// 修改
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
// 1. 执行副作用函数前,入栈
effectStack.push(effectFn)
fn()
// 2. 执行完出栈
effectStack.pop()
// 3. 出栈之后,还原activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
当前完整代码如下:
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 deps
if (!depsMap.get(key)) return
deps = depsMap.get(key)
const effectToRun = new Set(deps)
effectToRun.forEach(effect => {
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) {
// 修改
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
// 1. 执行副作用函数前,入栈
effectStack.push(effectFn)
fn()
// 2. 执行完出栈
effectStack.pop()
// 3. 出栈之后,还原activeEffect
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
解决无限循环问题
上一次提到无限循环问题,是在分支切换问题的时候有讲到,核心原因是Set的delete和add的共同作用。现在说到的无限循环问题,是指在副作用函数中,对响应式数据有做变更。当前的实现,由于没有拦截在Set中执行这个变更操作,会触发依赖收集和依赖触发的无限循环。
触发这个问题的代码如下:
effect(() => {
obj.text = obj.text + 'test'
})
为了解决这个问题,只需要在执行trigger的时候,排除当前activeEffect即可。
核心修改代码如下:
function trigger(target, key) {
...
// 不执行当前的注册的activeEffect即可
effects.forEach(effect => {
if (effect !== activeEffect) {
effectToRun.add(effect)
}
})
effectToRun.forEach(effect => {
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()
// 不执行当前的注册的activeEffect即可
effects.forEach(effect => {
if (effect !== activeEffect) {
effectToRun.add(effect)
}
})
effectToRun.forEach(effect => {
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) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}