react hooks 原理

使用 Hooks 时的疑惑

  1. React 如何管理区分Hooks?
  2. useState和useReducer如何在每次渲染时,返回最新的值?
  3. 为什么不能在条件语句等中使用Hooks?

React Hooks 源码入口

以 useState 为例,其他的等同

Mounted 阶段做了三步:

  1. 初始化状态,并返回修改状态的方法。
    利用 initialState 来初始化状态,返回状态和对应更新方法:
return [hook.memoizedState, dispatch]
  1. 区分管理每个Hooks
    通过mountWorkInProgressHook方法和Hook的类型定义
export type Hook = {
  memoizedState: any, // 指向当前渲染节点 Fiber
  baseState: any, // 初始化 initialState, 以及每次 dispatch 之后 newState
  baseUpdate: Update | null,
  queue: UpdateQueue | null,
  next: Hook | null,  // 指向下一个Hook,用来按顺序串联所有的 hook,可以看成一个 初始化为0 的cursor,每次进行+1
};

首先从Hook的类型定义中就可以看到,React 对Hooks的定义是链表。也就是说我们组件里使用到的Hooks是通过hash值链表来联系的,上一个Hooks的next指向下一个Hooks。这些Hooks节点通过mountWorkInProgressHook 方法将链表数据结构串联在一起,通过 next 按顺序串联所有的 hook。具体见代码
在mount阶段,每当我们调用Hooks方法,比如useState,mountState就会调用mountWorkInProgressHook 来创建一个Hook节点,并把它添加到Hooks链表上
以一段代码为例

const [age, setAge] = useState(initialAge);
const [name, setName] = useState(initialName);
useEffect(() => {})

那么在 mount 阶段,就会产生下面的链表图


  1. 返回最新的值
    通过使用一个 queue 链表来存放每一次的更新。以便后面的 update阶段可以返回最新的状态。每次我们调用dispatchAction方法的时候,就会形成一个新的updata对象,添加到queue链表上,而且这个是一个循环链表。
function dispatchAction() {
  const update: Update = {
    action,
    next: (null: any),
  };
// 将update对象添加到循环链表中
  const pending = queue.pending;
  if (pending === null) {
    // 链表为空,将当前更新作为第一个,并保持循环
    update.next = update;
  } else {
    // 将 最新的 pending 赋值给 最新的 update
    update.next = pending.next;
    // 在最新的update对象后面插入新的update对象
    pending.next = update;
  }
  // 确保queue.pending每次都是新的update对象
  queue.pending = update;
  // 进行调度工作
    scheduleWork(fiber, expirationTime);
}

也就是我们每次执行dispatchAction方法,比如对上面的State age执行setAge。就会创建一个保存着这次更新信息的update对象,添加到更新链表queue上。然后每个Hooks节点就会有自己的一个queque。

setAge(19);
setAge(20);
setAge(21);

我们的链表图就会变成

在Hooks节点上面,会如上图那样,通过链表来存放所有的历史更新操作。以便在update阶段可以通过这些更新获取到最新的值(也就是我们的 queue.pending)返回给我们。这就是在第一次调用useState或useReducer之后,每次更新都能返回最新值的原因。

update 阶段

function updateState(
  initialState: (() => S) | S,
): [S, Dispatch>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

updateState 其实也是调用 updateReducer,而 updateReducer 是需要传入一个 reducer 的,但我们使用 setState 的时候并不会传入 reducer,所以这里会默认给我们传入一个 basicStateReducer

  • basicStateReducer
function basicStateReducer(state, action){
  return typeof action === 'function' ? action(state) : action;
} 

在使用useState(action)的时候,action通常会是一个值,而不是一个方法。所以baseStateReducer要做的其实就是将这个action返回,因为传入的update.action为值,所以会直接返回update.action,而useReducer 的reducer是用户定义的reducer,所以会根据传入的action和每次循环得到的newState逐步计算出最新的状态。

通过上面的链表图流程图我们对一开始的问题有了答案:

  1. React 如何管理区分Hooks?
  • React通过单链表来管理Hooks
  • 按Hooks的执行顺序依次将Hook节点添加到链表中
  1. useState和useReducer如何在每次渲染时,返回最新的值?
  • 每个Hook节点通过循环链表记住所有的更新操作
  • 在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
  1. 为什么不能在条件语句等中使用Hooks?
    因为Hooks是通过单链表方式依次执行的,如果有一个条件不通过,那么我们的链表图就会对应不上,就会报错
    比如:
[A, setA] = useState('A')
if (count > 0) {
  [B, setB] = useState('B')
}
[C, setC] = useState('C')

我们在mount阶段调用了useState('A'), useState('B'), useState('C'),如果我们将useState('B') 放在条件语句内执行,并且在update阶段中因为不满足条件而没有执行的话,那么没法正确的重Hooks链表中获取信息

自己实现一个简易版的useState

let _state = [];
let index = 0;
const myUseState = num => {
  // 之所以要把index赋给一个currentIndex变量是因为,
  // 我们render完后紧接着要对index+1,这样就可以调用几次useState数组里面就有多少个state,
  // 也就是index就会和state对应
  const currentIndex = index;
  _state[currentIndex] =
    _state[currentIndex] === undefined ? num : _state[currentIndex];
  const setN = num1 => {
    _state[currentIndex] = num1;
    render();
  };
  index += 1;
  return [_state[currentIndex], setN];
};
function render() {
  // 之所以render前先置为0是因为如果不置0就会一直累加下去,而页面根本没有那么多state
  index = 0;
  ReactDOM.render(, rootElement);
}
function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  const x = () => {
    setN(n + 1);
  };
  const y = () => {
    setM(m + 1);
  };
  return (
    
{n}
{m}
); }

我们的Hooks链表存在哪里?

currentlyRenderingFiber.memoizedState = workInProgressHook = hook;

从上面代码可以看出 我们每次的 hooks 都会先存到 workInProgressHook 然后存到 currentlyRenderingFiber.currentlyRenderingFiber 而 currentlyRenderingFiber 的类型是一个 Fiber节点

useEeffect

用来处理副作用,几个特点:

  • 有两个参数 callback 和 dependencies 数组
  • 如果 dependencies 不存在,那么 callback 每次 render 都会执行
  • 如果 dependencies 存在,只有当它发生了变化, callback 才会执行
  • 如果 dependencies 是空数组,那么只有在 Mounted 阶段才会执行

useEffect 也分为 mountEffect 和 updateEffect

mountEffect

function mountEffect( create,deps,) {
  return mountEffectImpl(
    create,
    deps,
  );
}

function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
  // 获取当前Hook,并把当前Hook添加到Hook链表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
 // 把最新的effect 添加到 memoizedState 链表作为最后一项
  hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}

function pushEffect(tag, create, destroy, deps) {
  const effect: Effect = {
    tag, // 一个二进制数,它将决定effect的行为
    create, // 绘制后应该运行的回调
    destroy, // 从create()返回的回调应该在初始渲染之前运行
    deps, // 依赖项
    next: (null: any), // 下一个effect 引用
  };
  // componentUpdateQueue 会被挂载到fiberNode的updateQueue上
  if (componentUpdateQueue === null) {
    // 如果当前Queue为空,将当前effect作为第一个节点
    componentUpdateQueue = createFunctionComponentUpdateQueue();
   // 保持循环
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 否则,添加到当前的Queue链表中
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect; 
}

在mount阶段,useEffect做的事情就是将自己的effect添加到了componentUpdateQueue上。这个componentUpdateQueue会在renderWithHooks方法中赋值到fiberNode的updateQueue上。

export function renderWithHooks() {
   const renderedWork = currentlyRenderingFiber;
   renderedWork.updateQueue = componentUpdateQueue;
}

就是在mouted阶段effect和useState一样通过循环单链表的形式添加到一个updateQueue链表里,但是与useState不同的是它不会把最后一次的作为表头
比如:

function PersionInfo () {
  const [age, setAge] = useState(18);
  useEffect(() =>{
      console.log(age)
  }, [age])

 const [name, setName] = useState('Dan');
 useEffect(() =>{
      console.log(name)
  }, [name])
  return (
    <>
      ...
    
  );
}

updateEffect

function updateEffect(create,deps){
  return updateEffectImpl(
    create,
    deps,
  );
}

function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps){
  // 获取当前Hook节点,并把它添加到Hook链表
  const hook = updateWorkInProgressHook();
  // 依赖 
  const nextDeps = deps === undefined ? null : deps;
 // 清除函数
  let destroy = undefined;

  if (currentHook !== null) {
    // 拿到前一次渲染该Hook节点的effect
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 对比上一次和当前的依赖
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 如果依赖没有变化,就会传入一个NoHookEffect 作为 tag
        // effect的执行
        pushEffect(NoHookEffect, create, destroy, nextDeps);
        return;
      }
    }
  }
  hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}

update 阶段和 mounted 阶段类似,只不过这里会判断更新 effect 的依赖有没有变化,如果没有就会传入一个 NoHookEffect作为 tag,最后通过 tag 来判断是否执行 当前 effect

function commitHookEffectList(unmountTag,mountTag,finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & unmountTag) !== NoHookEffect) {
        // Unmount 阶段执行tag !== NoHookEffect的effect的清除函数 (如果有的话)
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      if ((effect.tag & mountTag) !== NoHookEffect) {
        // Mount 阶段执行所有tag !== NoHookEffect的effect.create,
        // 我们的清除函数(如果有)会被返回给destroy属性,一遍unmount执行
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

所以 useEffect 主要做了三步:
1). Fiber中会有一个updateQueue链表来存放所有的本次渲染需要执行的effect。
2). mountEffect阶段和updateEffect阶段会把effect push到updateQueue上。
3). updateEffect阶段,deps没有改变的effect会被打上NoHookEffect tag,commit阶段会跳过该Effect。

简易版 useEffect 实现

const memoizedState = []; // 存放每次传入的依赖
let cursor = 0;
function useEffect(callback, depArray) {
  const noDeps = !depArray;
  const deps = memoizedState[cursor];
  // 依赖有变化
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  // 如果没有依赖或者依赖发生了变化一调用我们的callback
  if (noDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

Hooks 和 Class 相比的优缺点?
优点:

  • 代码可读性更高
  • 代码量少
  • 不用考虑 this 指向问题

缺点:

  • 响应式的 useEffect
    如果我们对依赖项定义的不准确的话,useEffect触发的次数会比我们预想的多;
  • 状态不同步
    我们每次修改state都不会修改当前的state,而是会重新生成一个新的state,旧的state和新的state有可能同时存在,之后旧的state会被垃圾回收掉
function App() {
  const [n, setN] = React.useState(0)
  const log = () => {
    setTimeout(() => console.log(`n: ${n}`), 3000)
  }
  return (
    

{n}

) }

上面的代码有两种操作
1). 点击+1再点击log--> 得到的n的值就是当前的值+1
2). 点击log再点击+1--> 得到的值就是上一个n的值

在线代码运行:https://codesandbox.io/s/icy-firefly-imvfu

简易版 useState 原理:
https://www.jianshu.com/p/5c405a00ba06

你可能感兴趣的:(react hooks 原理)