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

2,951 阅读3分钟

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>
}

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];
}

当然事情没那么简单,实际上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];
}

那么为什么不能破坏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> 
   }

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++;
}

总结

  1. 更新是如何发生:

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

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

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