useEffect
实现 useEffect
期望的行为是
- useEffect 执行后,回调函数立即执行
- 依赖的自变量变化后,回调函数立即执行
- 不需要我们显示指明依赖
const [count, setCount] = useState(0)
useEffect(() => {
console.log(count)
})
useEffect(() => {
console.log('哈哈哈')
})
setCount(2)
// 期望打印顺序 先打印 0,然后打印 哈哈哈,然后count 改变,第一个effect内部依赖count, 然后打印 2
这里关键在于我们要建立起 useState 和 useEffect 的关系我们建立一个发布订阅关系:
- 当 useEffect 回调中执行 useState 的 getter 的时候,就让这个effect 订阅 该state 的变化
- 当 useStare 的 setter 执行的时候,就向订阅了他的 effect 发布通知
实现步骤
在 state 内部创建一个集合 subs,用来保存 订阅他变化的 effect, 将 effect 设置一个数据结构:
- 这样的话, 就可以通过遍历 state 的 subs 来找到所有订阅该 state 变化的 effect, 然后通过 effect 的 deps 找到所有 该 effect 依赖的 state.subs
const useEffect = (callback) => {
const execute = () => {
cleanup(effect); // 重置订阅发布依赖
effectStack.push(effect) // 将当前 effect 推入栈顶
try {
callback() // 执行回调
} finally {
effectStack.pop() // effect 出栈
}
}
const effect = {
execute,
deps: new Set()
}
execute(); // 立即执行一次建立关系
}
- 在 callback 执行前调用 cleanup 来 清除所有 与该 effect 相关的订阅发布关系,具体原因例子我们在下文解释, callback执行时会重建订阅发布关系。这为 细粒度更新 带来 自动依赖追踪能力,
function cleanup(effect) {
// 从该 effect 订阅的所有 state 对应 subs 中移除该effect
for (const subs of effect.deps) {
subs.delete(effect)
}
// 将该effect 依赖所有 state 对应 subs 移除
effect.deps.clear()
}
在调用 state 的 getter 时候,需要知道这个 state 当前是哪个effect上下文,主要是用来建立 effect 和 state 的联系。 所以callback 执行的时候将 effect 推入effectStack 栈顶,执行后出栈。在useState的getter 内部就可以通过获取栈顶元素得到当前所处的 effect 的上下文.
然后 useEffect 执行后内部执行execute, 首次建立订阅发布关系。这是自动收集依赖的关键
function useState(value) {
const subs = new Set() // 用来保存订阅该state的effect
const getter = () => {
// 获取当前上下文的effect
const effect = effectStack.at(-1);
if (effect) {
// 如果他处在上下文中,则需要建立订阅发布关系
subscribe(effect, subs)
}
return value
}
const setter = (nextValue) => {
value = nextValue;
// 执行订阅该state变化的effect执行
for (const effect of [...subs]) {
effect.execute()
}
}
return [getter, setter]
}
实现subscribe方法
function subscribe(effect, subs) {
subs.add(effect)
effect.deps.add(subs) // 建立订阅关系建立
}
上面实现了useState, useEffect 后,我们就可以在这个基础上实现useMemo
function useMemo(callback) {
const [value, setValue] = useState()
useEffect(() => setValue(callback())) // 首次执行callback, 建立回调中state的订阅发布关系
return value
}
现在我们来看下,为什么每次在effect 的 execute 执行 都需要重置订阅发布关系,我们来看下面的例子, 比如
const [name1, setName1] = useState('小金')
const [name2, setName2] = useState('小王')
const [show, setShow] = useState(true)
const whoSmile = useMemo(() => {
if (!show()) {
return name1()
}
return `${name1()} 和 ${name2()}`
})
useEffect(() => console.log('谁在那哈哈哈', whoSmile()))
setName1('小李')
setShow(false)
setName2('小杨')
打印如下:
- 谁在那哈哈哈 小金 和 小王
- 谁在那哈哈哈 小李 和 小王
- 谁在那哈哈哈 小李
- 不打印信息
我们可以看到,当 setShow 为 false 都时候,whoSmile 中的 name2 并没有执行,因此name2 和 whoSmile 并不存在了关系,只有 show() 为 true 的时候,whoSmile 才会重新依赖 name1 和 name2