[React] Hooks 实现原理是什么, 简单描述一下

在探索 useEffect 原理的时候,一直被一个问题困扰:useEffect 作用和用途是什么?当然,用于函数的副作用这句话谁都会讲。举个例子吧:

function App() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    // 模拟异步请求后端数据
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  }, []);

  return <div>{!num ? "请求后端数据..." : `后端数据是 ${num}`}</div>;
}

这段代码,虽然这样组织可读性更高,毕竟可以将这个请求理解为函数的副作用。但这并不是必要的。完全可以不使用useEffect,直接使用setTimeout,并且它的回调函数中更新函数组件的 state。

在 useEffect 的第二个参数中,我们可以指定一个数组,如果下次渲染时,数组中的元素没变,那么就不会触发这个副作用(可以类比 Class 类的关于 nextprops 和 prevProps 的生命周期)。好处显然易见,相比于直接裸写在函数组件顶层,useEffect 能根据需要,避免多余的 render

下面是一个不包括销毁副作用功能的 useEffect 的 TypeScript 实现:

// 还是利用 Array + Cursor的思路
const allDeps: any[][] = [];
let effectCursor: number = 0;

function useEffect(callback: () => void, deps: any[]) {
  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();
    allDeps[effectCursor] = deps; // 感谢 juejin@carlzzz 的指正
  }
  ++effectCursor;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  effectCursor = 0; // 注意将 effectCursor 重置为0
}

对于 useEffect 的实现,配合下面案例的使用会更容易理解。当然,你也可以在这个 useEffect 中发起异步请求,并在接受数据后,调用 state 的更新函数,不会发生爆栈的情况。

function App() {
  const [num, setNum] = useState < number > 0;
  const [num2] = useState < number > 1;

  // 多次触发
  // 每次点击按钮,都会触发 setNum 函数
  // 副作用检测到 num 变化,会自动调用回调函数
  useEffect(() => {
    console.log("num update: ", num);
  }, [num]);

  // 仅第一次触发
  // 只会在compoentDidMount时,触发一次
  // 副作用函数不会多次执行
  useEffect(() => {
    console.log("num2 update: ", num2);
  }, [num2]);

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}> 1</button>
        <button onClick={() => setNum(num - 1)}> 1</button>
      </div>
    </div>
  );
}

useEffect 第一个回调函数可以返回一个用于销毁副作用的函数,相当于 Class 组件的 unmount 生命周期。这里为了方便说明,没有进行实现。

参考文档: