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

February 13, 2022

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。