跳到主要内容

useEffect

实现 useEffect

期望的行为是

  1. useEffect 执行后,回调函数立即执行
  2. 依赖的自变量变化后,回调函数立即执行
  3. 不需要我们显示指明依赖
const [count, setCount] = useState(0)

useEffect(() => {
console.log(count)
})

useEffect(() => {
console.log('哈哈哈')
})

setCount(2)
// 期望打印顺序 先打印 0,然后打印 哈哈哈,然后count 改变,第一个effect内部依赖count, 然后打印 2

这里关键在于我们要建立起 useState 和 useEffect 的关系我们建立一个发布订阅关系:

  1. 当 useEffect 回调中执行 useState 的 getter 的时候,就让这个effect 订阅 该state 的变化
  2. 当 useStare 的 setter 执行的时候,就向订阅了他的 effect 发布通知

实现步骤

在 state 内部创建一个集合 subs,用来保存 订阅他变化的 effect, 将 effect 设置一个数据结构:

  1. 这样的话, 就可以通过遍历 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(); // 立即执行一次建立关系
}
  1. 在 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('小杨')

打印如下:

  1. 谁在那哈哈哈 小金 和 小王
  2. 谁在那哈哈哈 小李 和 小王
  3. 谁在那哈哈哈 小李
  4. 不打印信息

我们可以看到,当 setShow 为 false 都时候,whoSmile 中的 name2 并没有执行,因此name2 和 whoSmile 并不存在了关系,只有 show() 为 true 的时候,whoSmile 才会重新依赖 name1 和 name2