在《effect 范式下的组件状态和依赖》中,useState / useEffect 是被最多提及的。
useState / useEffect 是驱动 render 触发和运行的基础
那本文就从实现简单的 useState / useEffect 落脚,讨论下他们如何管理状态、驱动 render 的。
手动实现一个 useState
先回忆一下,useState 是怎么调用的?
const Comp = props => {
const [ foo, setFoo ] = useState(1);
return <button onClick={() => setFoo(2)}>{foo}</button>
};
useState 实现了:
- 传入一个初始状态
- 返回一个数组,包含最新状态和 set 状态的方法。
- 调用 set 状态的方法时,会触发重新渲染
很容易实现
let currentState;
const useState = initialState => {
const state = currentState || initialState;
const setState = newState => {
currentState = newState;
render(); // 这个 render 可以理解为触发了整个 react app 渲染,就像 ReactDOM.render();
};
return [ state, setState ];
};
收纳盒:兼容多次调用
事情没这么简单,实际上 useState 在整个 app,甚至单个组件内通常都不会只调用一次,那调用的都是同一个 useState 实现,如何区分调用者呢?
假设代码内调用 useState 的顺序是不变的
在通常情况下确实是这样的。那就好办了,按顺序存到盒子里就好。
就像每周(一次渲染)七天穿不同的袜子(七次 useState)。把他们放到收纳盒里,每天去下一个格子取就好;下一周(下一次渲染周期)再从头开始。
let index = -1;
const currentStateBoxs = [];
const useState = initialState => {
index++;
const currentIndex = index;
currentStateBoxs[currentIndex] = currentStateBoxs[currentIndex] || initialState;
const setState = newState => {
currentStateBoxs[currentIndex] = newState;
render(); // 这个 render 可以理解为触发了整个 react app 渲染,就像 ReactDOM.render();
};
return [ currentStateBoxs[currentIndex], setState ];
};
为什么说不要破坏 useState 的顺序?
试想如果有个周二你没穿袜子,周三就穿错了……
const Comp = props => {
const [ 周一袜子 ] = useState(xx); // 拿到第一个盒子的袜子
if (周二天气好) {
const [ 周二袜子 ] = useState(xx);
}
const [ 周三袜子 ] = useState(xx); // 如果周二没拿,周三拿了第二个盒子的袜子
};
这里问题出在 if,它让 useState 的执行顺序出现了不确定性,类似的还有 for 等其他条件、循环语句。
手动实现一个 useEffect
useEffect 是怎么用的呢?
const Comp = props => {
const [ foo, setFoo ] = useState(1);
useEffect(() => {
...
}, [ foo ]);
return <button>{foo}</button>
};
useEffect 在依赖变化时,执行回调函数。这个变化,是「本次 render 和上次 render 时的依赖比较」。
我们需要:
- 存储依赖,上一次 render 的
- 兼容多次调用,也是收纳盒的思路
- 比较依赖,执行回调函数
实现:
const lastDepsBoxs = [];
let index = 0;
const useEffect = (callback, deps) => {
const lastDeps = lastDepsBoxs[index];
const changed =
!lastDeps // 首次渲染,肯定触发
|| !deps // deps 不传,次次触发
|| deps.some((dep, index) => dep !== lastDeps[index]); // 正常比较
if (changed) {
lastDepsBoxs[index] = deps;
callback();
}
index ++;
};
增加副作用清除
在另外一文提到过,effect 触发后会把清除函数暂存起来,等下一次 effect 触发时执行。
明确这个顺序就不难实现了:
const lastDepsBoxs = [];
const lastClearCallbacks = [];
let index = 0;
const useEffect = (callback, deps) => {
const lastDeps = lastDepsBoxs[index];
const changed = !lastDeps || !deps || deps.some((dep, index) => dep !== lastDeps[index]);
if (changed) {
lastDepsBoxs[index] = deps;
if (lastClearCallbacks[index]) {
lastClearCallbacks[index]();
}
lastClearCallbacks[index] = callback();
}
index ++;
};
总结
- 利用闭包,useState / useEffect 的实现并不深奥
- 巧妙的是对多次调用的组织方式,“收纳盒”
- 但“收纳盒”对顺序敏感,所以使用 hooks 要避免 if、for 等嵌套