react hooks原理
- useState 的实现原理
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
let state: any;
export function useState<T>(initState: T): [T, (newState: T) => void] {
state = state || initState;
function setState(newState: T) {
state = newState;
// 改变新值后 render
render();
}
return [state, setState];
}
// 首次render
render()
- 循环下或者判断下调用
const App = () => {
const [count, setCount] = useState(0);
if (count == 0) {
useEffect(() => {
console.log(count);
}, [count]);
}
return (
<>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
</>
);
};
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
前面 useState 的简单实现里,初始的状态是保存在一个全局变量中的。以此类推,多个状态,应该是保存在一个专门的全局容器中。这个容器,就是一个朴实无华的 Array 对象。具体过程如下:
- 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
- 更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。
let flag = true;
const App = () => {
const [count, setCount] = useState(0);
if (flag) {
const [unusedCount] = useState(1);
flag = false;
}
const [count2, setCount2] = useState(0);
return (
<>
<div>{count}</div>
<div>{count2}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount2(count2 + 1)}>+1</button>
</>
);
};
由于在条件判断的逻辑中,重置了flag = false
,因此此后的渲染不会再进入条件判断语句。看起来好像没有问题?但是,由于 useState 是基于 Array + Cursor 来实现的,第一次渲染时候,state 和 cursor 的对应关系如下表:
变量名 | cursor |
---|---|
count | 0 |
unusedCount | 1 |
count2 | 2 |
当点击事件触发再次渲染,并不会进入条件判断中的 useState
。所以,cursor=2 的时候对应的变量是 count2
。而其实 count2
对应的 cursor
应该是 3。就会导致setCount2
并不起作用。也就是说,每次渲染后通过 cursor
在查找对应的 state
会出现乱序的现象
- useEffect 的实现原理
useEffect 是被称为副作用,但是正确的理解是根据一个依赖改变,会对影响组件的内部状态,其思想来源于函数式编程。
具体 useEffect 用法看参考:useEffect 完整指南
在 useEffect 的第二个参数中,我们可以指定一个数组,如果下次渲染时,数组中的元素没变,那么就不会触发这个副作用(可以类比 Class 类的关于 nextprops 和 prevProps 的生命周期)。好处显然易见,相比于直接裸写在函数组件顶层,useEffect 能根据需要,避免多余的 render
// 还是利用 Array + Cursor的思路
const allDeps: any[][] = [];
// effect个数,按照顺序调用
let effectCursor: number = 0;
function useEffect(callback: () => void, deps: any[]) {
// 初次渲染:统计依赖->确定 effect 顺序->调用回调函数
if (!allDeps[effectCursor]) {
allDeps[effectCursor] = deps;
++effectCursor;
callback();
return;
}
const currenEffectCursor = effectCursor;
const rawDeps = allDeps[currenEffectCursor];
// 检测依赖项是否发生变化,发生变化需要重新render
const isChanged = rawDeps.some(
(dep: any, index: number) => dep !== deps[index]
);
if (isChanged) {
callback();
}
++effectCursor;
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
effectCursor = 0; // 注意将 effectCursor 重置为0
}
- Class VS Hooks | Class | Hooks | | —- | —- | | 学习成本较高,需要记住生命周期一些新概念 | 更加灵活,开发者可以自定义方法 | | 代码复用可以使用HOC与renderProps | 自定义hooks需要开发对逻辑有更清晰掌握 | | 不容易发生内存泄漏 | 闭包大量使用会影响性能 | | 代码结构清晰(生命周期),心智负担小 | 需要配合变量名和注释,心智负担大 |