react hooks原理

  • useState 的实现原理
  1. function render() {
  2. ReactDOM.render(<App />, document.getElementById("root"));
  3. }
  4. let state: any;
  5. export function useState<T>(initState: T): [T, (newState: T) => void] {
  6. state = state || initState;
  7. function setState(newState: T) {
  8. state = newState;
  9. // 改变新值后 render
  10. render();
  11. }
  12. return [state, setState];
  13. }
  14. // 首次render
  15. render()
  • 循环下或者判断下调用
  1. const App = () => {
  2. const [count, setCount] = useState(0);
  3. if (count == 0) {
  4. useEffect(() => {
  5. console.log(count);
  6. }, [count]);
  7. }
  8. return (
  9. <>
  10. <div>{count}</div>
  11. <button onClick={() => setCount(count + 1)}>+1</button>
  12. </>
  13. );
  14. };
  1. 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 的值,视图更新。
  1. let flag = true;
  2. const App = () => {
  3. const [count, setCount] = useState(0);
  4. if (flag) {
  5. const [unusedCount] = useState(1);
  6. flag = false;
  7. }
  8. const [count2, setCount2] = useState(0);
  9. return (
  10. <>
  11. <div>{count}</div>
  12. <div>{count2}</div>
  13. <button onClick={() => setCount(count + 1)}>+1</button>
  14. <button onClick={() => setCount2(count2 + 1)}>+1</button>
  15. </>
  16. );
  17. };

由于在条件判断的逻辑中,重置了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

  1. // 还是利用 Array + Cursor的思路
  2. const allDeps: any[][] = [];
  3. // effect个数,按照顺序调用
  4. let effectCursor: number = 0;
  5. function useEffect(callback: () => void, deps: any[]) {
  6. // 初次渲染:统计依赖->确定 effect 顺序->调用回调函数
  7. if (!allDeps[effectCursor]) {
  8. allDeps[effectCursor] = deps;
  9. ++effectCursor;
  10. callback();
  11. return;
  12. }
  13. const currenEffectCursor = effectCursor;
  14. const rawDeps = allDeps[currenEffectCursor];
  15. // 检测依赖项是否发生变化,发生变化需要重新render
  16. const isChanged = rawDeps.some(
  17. (dep: any, index: number) => dep !== deps[index]
  18. );
  19. if (isChanged) {
  20. callback();
  21. }
  22. ++effectCursor;
  23. }
  24. function render() {
  25. ReactDOM.render(<App />, document.getElementById("root"));
  26. effectCursor = 0; // 注意将 effectCursor 重置为0
  27. }
  • Class VS Hooks | Class | Hooks | | —- | —- | | 学习成本较高,需要记住生命周期一些新概念 | 更加灵活,开发者可以自定义方法 | | 代码复用可以使用HOC与renderProps | 自定义hooks需要开发对逻辑有更清晰掌握 | | 不容易发生内存泄漏 | 闭包大量使用会影响性能 | | 代码结构清晰(生命周期),心智负担小 | 需要配合变量名和注释,心智负担大 |