Files
DefectingCat.github.io/content/posts/automatic-dependency-collect.mdx
2024-04-16 16:12:18 +08:00

279 lines
8.6 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 细颗粒度更新与自动依赖收集
date: '2023-11-09'
tags: [TypeScript, React]
---
众所周知React 并没有实现细颗粒度的状态更新。同时在副作用中也需要我们手动去管理对应依赖。如常见的 `useEffect` 和 `useMemo` 等:
```ts
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count updated ', count);
}, [count]);
```
这里就需要我们手动的去管理 `useEffect` 的依赖,否则 `useEffect` 则只会执行一次。而在 Vue 等框架中,则可以自动追踪其依赖:
```ts
const y = coumputed(() => x.value * 2 + 1);
```
在 Vue 和 Mobx 等框架中使用的“能自动追踪依赖的技术”被称为“细颗粒度更新”Fine-Grained Reactivity这也是许多前端框架响应式的原理。
## 实现一个细颗粒度更新
我们希望为 React 实现一个简易的细颗粒度更新的状态,使我们在使用副作用时不需要手动管理其依赖的 hooks。
其设想的效果应如下:
```ts
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count is ', count());
});
useEffect(() => {
console.log('only print once');
});
// Trigger effect
setCount(1);
```
`useEffect` 不再需要为其手动管理状态,而是自动追踪依赖。当我们更新状态 `count` 时,自动触发 `useEffect` 的回调。
### useState
第一步就是现实一个简易的状态,这里模仿 React 官方 `useState` 的签名,我们也返回一个元组,分别是状态和更新其方法。
```ts
export type Getter<T> = () => T;
export type Setter<T> = (newValue: T | ((oldValue: T) => T)) => void;
export type UseState = <T>(value: T) => [Getter<T>, Setter<T>];
const useState: UseState = (value) => {
const getter: Getter<typeof value> = () => {
return value;
};
const setter: Setter<typeof value> = (updater) => {
const newState = updater instanceof Function ? updater(value) : updater;
if (value === newState) return;
value = newState;
};
return [getter, setter];
};
export { useState };
```
这样看似很美好,我们利用函数的参数和闭包实现了一个状态,并在调用设置状态函数时,更新该状态。在调用 `getter()` 时返回该状态。但是它无法和 React 一起使用,因为 React 的 `useState` 重点不仅仅是对一个值的设置,而是在状态更新时更新 React 组件。我们的 `useState` 只实现了一个功能,对状态的管理,我们还需要实现更新 React 组件才能和 React 一起使用。
我们的重点不是在如何实现一个 React所以更新组件就交给真正的 `useState` 来做。我们通过在我们自己状态更新时,调用 React 的 `useState` 来更新组件。这样就通过借用官方的 `useState` 来帮助我们更新组件,从而实现一个简易的状态。
```ts
const useState: UseState = (value) => {
const state = useRef(value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, update] = useUpdate({});
const getter: Getter<typeof value> = () => {
return state.current;
};
const setter: Setter<typeof value> = (updater) => {
const newState =
updater instanceof Function ? updater(state.current) : updater;
if (state.current === newState) return;
state.current = newState;
update({});
};
return [getter, setter];
};
```
使用它的方式和官方 hooks 没有区别:
<RUASandpack
template="react-ts"
files={{
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/signal.ts': sandpack['automatic-dependency-collect'].signal1,
'/App.tsx': sandpack['automatic-dependency-collect'].app1,
}}
/>
### useEffect
有了状态之后接下来就是副作用了。Effect 主要需要解决的问题就是如何自动收集依赖,例如:
```ts
useEffect(() => {
console.log('count is ', count());
});
```
在用户提供的回调函数中,我们无法直接了当的去知道其中是否有状态需要的依赖,也无法知道有哪些状态在其中。这就是为什么我们将获取状态的 `getter` 设计成了一个函数,通过在 effect 中调用的 `getter` ,我们就能为 `setter` 和 effect 之间建立一个订阅关系,从而实现调用 `setter` 时调用 effect 的回调方法。
首要方法就是订阅发布模型,首先我们需要为 effect 创建一个用于回调调用栈,当 `useEffect` 第一次调用时,我们将该 effect 对应的信息保存到栈中。随后如果在回调函数中有状态的 `getter` 该 `getter` 则会收集栈中的 effect 并创建对应的订阅。
第一步就是创建保存到栈中的数据结构与一个全局的用于保存 effect 的栈:
```ts
export type Effect = {
// 用于执行 useEffect 回调函数
execute: () => void;
// 保存该 useEffect 依赖的 state 对应的 subs 集合
deps: Set<unknown>;
};
const effectStack: Effect[] = [];
```
随后来定义我们的 `useEffect`
```ts
const useEffect = (callback: () => void) => {
const execute = () => {
effectStack.push(effect);
try {
callback();
} finally {
effectStack.pop();
}
};
const effect: Effect = {
execute,
deps: new Set(),
};
// 调用时执行
execute();
};
```
`execute` 用于将当前的 effect 推入栈中保存,随后执行用户的回调函数。我们的依赖收集重点就是在这里,如果回调函数 `callback` 中有我们状态的 `getter`,那么 `getter` 将会收集刚刚推入栈中的 effect 并建立订阅管理,从而实现自动收集依赖。
所以这里的 `getter` 需要小小的更新下:
```ts
const subscribe = (effect: Effect, subs: Subs) => {
// 建立订阅关系
subs.add(effect);
// 建立依赖关系
effect.deps.add(effect);
};
const useState: UseState = (value) => {
const state = useRef(value);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, update] = useUpdate({});
const subs = useRef(new Set<Effect>());
const getter: Getter<typeof value> = () => {
// 获取刚刚送入栈中的 effect
const effect = effectStack[effectStack.length - 1];
if (effect) {
// 建立订阅发布关系
subscribe(effect, subs.current);
}
return state.current;
};
const setter: Setter<typeof value> = (updater) => {
const newState =
updater instanceof Function ? updater(state.current) : updater;
if (state.current === newState) return;
state.current = newState;
update({});
};
return [getter, setter];
};
```
这里通过给 `getter` 添加一个在执行时检查 `effectStack`,如果能够取到栈尾的 effect 则添加到 `useState` 的订阅列表 `subs` 中。
接下来就是更新 `setter`,使其在更新状态时检查订阅列表 `subs` 并遍历执行列表中所有副状态:
```ts
const setter: Setter<typeof value> = (updater) => {
const newState =
updater instanceof Function ? updater(state.current) : updater;
if (state.current === newState) return;
state.current = newState;
update({});
for (const sub of [...subs.current]) {
sub.execute();
}
};
```
除此之外,和 `useEffect` 同理,我们需要放到 React 组件中使用,而我们刚刚设计的状态也会真正的更新一个函数式组件。由于函数式组件的特殊性,每次组件更新时所有的 hook 都会执行,所以我们的 `useEffect` 还需要 React 的真正的 `useEffect` 一点小小的帮助。
```ts
const useEffect = (callback: () => void) => {
const execute = () => {
// 重制依赖
cleanup(effect);
// 添加到副作用列表
effectStack.push(effect);
try {
callback();
} finally {
effectStack.pop();
}
};
const effect: Effect = {
execute,
deps: new Set(),
};
useMounted(() => {
// 调用时执行
execute();
}, []);
};
```
<RUASandpack
template="react-ts"
files={{
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/signal.ts': sandpack['automatic-dependency-collect'].signal2,
'/App.tsx': sandpack['automatic-dependency-collect'].app2,
}}
/>
### useMemo
通过 `useEffect` 实现的自动依赖追踪,我们就可以轻松的实现一个自动追踪依赖的 `useMemo`
```ts
const useMemo = <T>(callback: () => T) => {
const [s, set] = useState(callback());
useEffect(() => set(callback()));
return s;
};
```
<RUASandpack
template="react-ts"
files={{
'/Button.tsx': {
code: sandpack.common.Button,
hidden: true,
},
'/signal.ts': sandpack['automatic-dependency-collect'].signal3,
'/App.tsx': sandpack['automatic-dependency-collect'].app3,
}}
/>