理解hooks原理 ---简单实现useState/useEffect

2022年2月13日

Hooks 是什么?

Hook  是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其 他的 React 特性。也是一个特殊的函数,它可以让你“钩入” React 的特性。例如,useState 是允许你在 React 函数组件中添加 state 的 Hook。

在《effect 范式下的组件状态和依赖》中,useState/useEffect 是被最多提及的。

useState / useEffect 是驱动 render 触发和运行的基础

useState 是如何实现的?

先回想下,useState 是如何调用的?

const Components = () => {
  const [value, setValue] = useState(0);
  return <button onClick={() => setValue(1)}>{value}</button>;
};
javascript

useState 实现了:

  • 传入了一个初始状态
  • 返回一个数组,一个初始值和调用 set 更新之后的值
  • 调用 set 方法时,替换原来 state 状态,类似于 class 组件里 this.setState
// 定义初始值
let currenInitValue;
function _UseState(initialValue) {
  // 输入值/默认初始值
  const state = currenInitValue || initialValue;

  const setState = (newValue) => {
    // 将新的值重新覆盖 更新state
    currenInitValue = newValue;
    // 触发视图更新
    render();
  };
  // 返回数组形式,解构可写成任意变量
  return [state, setState];
}
javascript

当然事情没那么简单,实际上 useState 在整个 app 中,甚至时单个组件内都通常都不会值调用一次,那将如何实现呢?当然不要破坏 useState 执行顺序。

// 下标
let index = 0;
// 利用收纳盒原理。存储调用者不同的存储值
let currenInitValueBox = [];
function _UseState(initialValue) {
  // 每用一次进行➕1
  index++;
  // 利用闭包维护函数调用位置
  const currentIndex = index;
  currenInitValueBox[currentIndex] =
    currenInitValueBox[currentIndex] || initvalue;
  const setValue = (newValue) => {
    // 更新state
    currenInitValueBox[currentIndex] = newValue;
    // 触发视图更新
    render();
  };
  return [currenInitValueBox[currentIndex], setValue];
}
javascript

那么为什么不能破坏 useState 的顺序呢?

从实现来看,每次 hook 的执行,都是从索引为 0 即第一个 hook 开始执行。也是依靠索引记录当前操作的 Hook,假如使用条件语句或者循环,那么 hook 执行的顺序可能与我们在数组中存放的顺序不一致,就会乱掉。因此不能在条件语句或循环中使用 Hook。

useEffect 如何实现?

useEffect 是如何调用的?

const Components = () => {
    const [value, setValue] = useState(0);
    useEffect(() => {...}, [value])
    return <button onClick={() => setValue(1)}>{value}</button>
   }
javascript

useEffect 原理是什么?

useEffect 在依赖发生变化时,执行回调函数,这个变化是本次 render 和上次 render 时的依赖比较当然我们需要:

  • 参数是回调函数,依赖以数组的形式
  • 存储上一次 render 时的依赖
  • 兼容多次调用,同一个组件下可能会有多次使用
  • 比较本次 render 和上一次 render 依赖,执行回调
  • 增加副作用清除(effect 触发后会将清除函数暂存起来,等下次触发时执行)
let index = 0;
// 同一组件下可能会出现多个useEffect使用,以数组的形式存储
let lastDepsBox = [];
let lastClearFnCallback = [];
/**
 *
 * @param {callback} fn 回调函数
 * @param {Array} deps 依赖
 */
function UseEffect(fn, deps) {
  // 存储上一次的依赖 存储的是[[]、[]、[]]
  const lastDeps = lastDepsBox[index];
  // 记录状态变化
  const flag =
    !lastDeps || // 首次渲染 刚开始就会触发
    !deps || // 没有依赖,次次触发
    deps.some((dep, index) => dep !== lastDeps[index]); // 依赖进行比较
  if (flag) {
    lastDepsBox[index] = deps;
    // effect触发后会将清除函数暂存起来,等下次触发时执行
    if (lastClearFnCallback[index]) {
      lastClearFnCallback[index]();
    }
    // 将清除函数暂存起来
    lastClearFnCallback[index] = fn();
  }
  index++;
}
javascript

总结

  1. 更新是如何发生:

调用useState,内部通过setState修改状态后,调用scheduleUpdate方法,从根节点执行完整的 dom-diff 比较,进行组件的更新。

  1. 为什么不能再条件语句或循环中使用 Hook

从实现来看,每次 hook 的执行,都是从索引为 0 即第一个 hook 开始执行。也是依靠索引记录当前操作的 Hook,假如使用条件语句或者循环,那么 hook 执行的顺序可能与我们在数组中存放的顺序不一致,就会乱掉。因此不能在条件语句或循环中使用 Hook。