image.png

前言

2021.11.7 立冬 , 北京昨晚一夜雨夹雪,气温骤然下降,闲着无事,看一看Hooks的源码吧

Hooks源码量并不大,相信很多小伙伴在网上都了解过Hooks的大概实现。看别人的文章,只能体会到九牛一毛。
古人云:’纸上得来终觉浅 绝知此事要躬行’, 那我们就从源码开始,揭开Hooks神秘的面纱吧!

React版本:17.0.2
调试源码见:github React源码调试demo

前置知识

如果你对Hooks的基本使用以及链表的基本知识还不清楚,请移步:

  1. React Hooks基本使用 React Hooks
  2. 链表相关 算法-链表

本文默认你已经对Fiber架构、双缓冲等概念比较了解,如果不了解,需要补课了 Fiber架构

Hooks疑问

在使用Hooks的时候,大家可能都会有疑问,为什么Hooks不可以在条件语句中使用呢?Class组件可以将状态挂载到实例上,Hooks本质上就是函数,那么它是怎么记住状态state的呢?函数式组件有那么多Hooks,他是怎么管理区分Hooks的呢?下面罗列了我在使用过程中的一些疑问,本文也是围绕Hooks的源码,针对这些疑问一一解答:

  • 1 在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
  • 2 多个hooks用什么来记录每一个hooks的顺序的 ? 换个问法!为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?
  • 3 function函数组件中的useState,和 class类组件 setState有什么区别?
  • 4 react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
  • 5 useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
  • 6 useMemo是怎么对值做缓存的?如何应用它优化性能?
  • 7 为什么两次传入useState的值相同,函数组件不更新?
  • 8 useState和useReducer如何在每次渲染时,返回最新的值?
  • 9 useRef是如何在函数式组件重新渲染的时候,能访问到最新的值?
  • 10 为什么useEffect的回调不能用async?

源码探索之旅

什么是Hook?
我们使用的useState、useEffect、useMemo、useRef等都会生成一个Hook对象,Hook类型定义在
my-debug-react/src/react/packages/react-reconciler/src/ReactFiberHooks.old.js文件中

hook定义

  1. export type Hook = {|
  2. memoizedState: any, // 保持在内存中的局部状态 -上次渲染之后的state
  3. baseState: any, // 通过处理好的update,计算出来的state
  4. baseQueue: Update<any, any> | null,// 存储update对象的环形链表, 只包括高于本次渲染优先级的update对象.
  5. queue: UpdateQueue<any, any> | null,// 存储update对象的环形链表, 包括所有优先级的update对象
  6. next: Hook | null,//指向下一个Hook
  7. |};

无论是挂载还是更新,都会产生hook,产生的hook对象依次排列,形成链表存储到函数组件fiber.memoizedState上。在这个过程中,有一个十分重要的指针:workInProgressHook,它通过记录当前生成(更新)的hook对象,可以间接反映在组件中当前调用到哪个hook函数了。每调用一次hook函数,就将这个指针的指向移到该hook函数产生的hook对象上。

image.png

不同的hook方法,存贮在memoizedState 中的信息不一样,

useState/useReducer state
useEffect/useLayoutEffect effect对象
useMemo/usecallback [callback,deps]

这里有个大概印象,后续会对每种的useXX进行梳理,自然就明白了

useState

我们都知道useState是useReducer的阉割版,我们就先从useState开始探索

useState、useEffect、useRef等Hooks是从 my-debug-react/src/react/packages/react/index.js 包中导出,沿着这条线往下看,找到了useState的位置
my-debug-react/src/react/packages/react/src/ReactHooks.js

export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {

  const dispatcher = resolveDispatcher();

  console.log('====> useState',initialState)

  return dispatcher.useState(initialState);
}

从源码中可以看到,我们调用的其实是 ReactCurrentDispatcher.js 中的dispatcher.useState(),那什么是dispatcher呢?沿着文件路径找下去 my-debug-react/src/react/packages/react-reconciler/src/ReactInternalTypes.js

export type Dispatcher = {|
  getCacheForType?: <T>(resourceType: () => T) => T,
  readContext<T>(context: ReactContext<T>): T,
  useState<S>(initialState: (() => S) | S): [S, Dispatch<BasicStateAction<S>>],
  useReducer<S, I, A>(
    reducer: (S, A) => S,
    initialArg: I,
    init?: (I) => S,
  ): [S, Dispatch<A>],
  useContext<T>(context: ReactContext<T>): T,
  useRef<T>(initialValue: T): {|current: T|},
  useEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useLayoutEffect(
    create: () => (() => void) | void,
    deps: Array<mixed> | void | null,
  ): void,
  useCallback<T>(callback: T, deps: Array<mixed> | void | null): T,
  useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T,
  ...
|};

这里我们可以看到,不同的useXX会执行对应Dispatch里的dispatch。

useState入口

类组件的setState和函数组件的useState有什么区别呢?前文中我们已经介绍过,useState在异步处理方面的差异。

从之前的源码梳理中 我们知道, 调用顺序是 beginWork —> updateFunctionComponent —> renderWithHooks, 这个函数是FunctionComponent的render主函数。

//my-debug-react/src/react/packages/react-reconciler/src/ReactFiberHooks.old.js

// ...省略无关代码
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  // --------------- 1. 设置全局变量 -------------------
  renderLanes = nextRenderLanes; // 当前渲染优先级
  currentlyRenderingFiber = workInProgress; // 当前fiber节点, 也就是function组件对应的fiber节点

  // 清除当前fiber的遗留状态
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  // --------------- 2. 调用function,生成子级ReactElement对象 -------------------
  // 指定dispatcher, 区分mount和update
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
  // 执行function函数, 其中进行分析Hooks的使用
  let children = Component(props, secondArg);

  // --------------- 3. 重置全局变量,并返回 -------------------
  // 执行function之后, 还原被修改的全局变量, 不影响下一次调用
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;
  didScheduleRenderPhaseUpdate = false;

  return children;
}
  1. 调用function前: 设置全局变量, 标记渲染优先级和当前fiber, 清除当前fiber的遗留状态.
  2. 调用function: 构造出Hooks链表, 最后生成子级ReactElement对象(children).
  3. 调用function后: 重置全局变量, 返回children.调用Component(props, secondArg);执行函数组件,hooks被依次执行,把hooks信息依次保存到workInProgress树上
    • 为了保证不同的function节点在调用时renderWithHooks互不影响, 所以退出时重置全局变量.

看21行,

  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
    ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。 useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象

HooksDispatcherOnMount 和 HooksDispatcherOnUpdate,它们内部的hooks函数是不同的实现,区别之一在于不同阶段对于hooks链表的处理是不同的。初次挂载时要创建链表,后续更新的时候要更新链表

挂载阶段

挂载的时候,执行线路 HooksDispatcherOnMount => mountState => mountWorkInProgress

初次挂载时,组件上没有任何hooks的信息,所以,这个过程主要是在fiber上创建hooks链表。挂载调用的是mountWorkInProgressHook,它会创建hook并将他们连接成链表,同时更新workInProgressHook,最终返回新创建的hook,也就是hooks链表。

// react-reconciler/src/ReactFiberHooks.js
function mountState (initialState) {
  // 1. 创建hook
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // 2. 初始化hook的属性
  // 2.1 设置 hook.memoizedState/hook.baseState
  // 2.2 设置 hook.queue
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer,
    lastRenderedState,
  });
  // 2.3 设置 hook.dispatch
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  )));
  // 3. 返回[当前状态, dispatch函数]
  return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // 当前workInProgressHook链表为空的话,
    // 将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

注释:currentlyRenderingFiber就是workInProgress节点

可以看到,mount的时候,主要:
1. 创建hook节点
2.将hook节点添加到hook链表上
3.将hook链表挂载到fiberNode的 memoizedState 上

来吧 我们看一下demo

function HooksDemo() {
  const [name, setName] = React.useState("Guan");
  const [age, setAge] = React.useState(18);
  const [color, setColor] = React.useState("red");

  const handleChange = () => {
    setName(name + "~qing");
    setName(name + "~chao");
  };
  const handleChangeAge = () => {
    setAge(100);
    setAge(200);
    setAge(300);
  };

  return (
    <div>
      Hooks
      <div>This is name:{name} </div>
      <div>This is age:{age} </div>
      <div>My favourite Color is: {color}</div>
      <br />
      <br />
      <br />
      <button onClick={handleChange}>点击改变Name</button>
      <button onClick={handleChangeAge}>点击改变Age</button>
    </div>
  );
}

本demo中我们定义了多个状态,打印看下hook
image.png

可以看到,mount的时候,

  • 每次调用useState的时候,都会生成一条hook记录
  • 多次调用useState的时候会形成一条hook链表

本示例中创建了3个hook,3个hook通过next进行连接形成hook链表, 如下:

image.png

继续看源码,moutState的时候,会返回当前的状态和改变状态的dispatch,继续看dispatchAction

更新阶段

对于更新过程中,由于存在current树,所以workInProgress节点也就有对应的current节点。

对于一次函数式组件的更新,当再次执行hooks函数的时候,例如useState(‘Joy’)

  1. 首先从current的hooks中找到与当前workInProgressHook对应的 currentHook,复制一份给workInProgressHook
  2. hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失

current树中对应的Hook指针为 currentHook,复用currentHook来创建新的hook对象

image.png

当调用setName、setAge的时候会触发dispatchAction

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 1. 创建update对象
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber); // Legacy模式返回SyncLane
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // 2. 将update对象添加到hook.queue.pending队列
  const pending = queue.pending;
  if (pending === null) {
    // 首个update, 创建一个环形链表 
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  console.log('Hooks ====> dispatchAction =====>',queue,update);
  const alternate = fiber.alternate;
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // 渲染时更新, 做好全局标记
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    // ...省略性能优化部分
     if (is(eagerState, currentState)) {
       return;
     }
    // 3. 发起调度更新, 进入`reconciler 运作流程`中的输入阶段.
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

逻辑十分清晰了

  1. 创建update对象, 其中update.lane代表优先级,每一次dispatch都会生成一个update对象
  2. 将update对象添加到hook.queue.pending环形链表.
    • 环形链表的特征: 为了方便添加新元素和快速拿到队首元素(都是O(1)), 所以pending指针指向了链表中最后一个元素. (稍后图解)
  3. 发起调度更新: 调用scheduleUpdateOnFiber, 进入reconciler 运作流程中的输入阶段.

看37行,React会根据最新的state和上一次的state进行浅比较,如果相同,就会跳过。
这就证实了为什么useState,两次值相等的时候,组件不渲染的原因了,这个机制和Component模式下的setState有一定的区别。

if (is(eagerState, currentState)) {
   return;
}

我们先来点击本例子中【点击改变Name】

demo1:
setName(‘qing’)
image.png
demo2:
setName(‘qing’),setName(‘chao’)
image.png
image.png

注:多次执行dispatch,由于调度中心优化,最后只会执行一次渲染

demo3:
【点击改变Age】触发3次dispatch setAge(100),setAge(200),setAge(300)
image.png

在fiber树构造(对比更新)过程中,再次调用function,调用路线 HooksDispatcherOnUpdate => updateState => updateReducer

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 1. 获取workInProgressHook对象
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current: Hook = (currentHook: any);
  let baseQueue = current.baseQueue;

  // 2. 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  // 3. 状态计算
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;

    do {
      const updateLane = update.lane;
      // 3.1 优先级提取update
      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 优先级不够: 加入到baseQueue中, 等待下一次render
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          eagerReducer: update.eagerReducer,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        // 优先级足够: 状态合并
        if (newBaseQueueLast !== null) {
          // 更新baseQueue
          const clone: Update<S, A> = {
            lane: NoLane,
            action: update.action,
            eagerReducer: update.eagerReducer,
            eagerState: update.eagerState,
            next: (null: any),
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        if (update.eagerReducer === reducer) {
          // 性能优化: 如果存在 update.eagerReducer, 直接使用update.eagerState.避免重复调用reducer
          newState = ((update.eagerState: any): S);
        } else {
          const action = update.action;
          // 调用reducer获取最新状态
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    // 3.2. 更新属性
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    // 把计算之后的结果更新到workInProgressHook上
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

updateWorkInProgress主要功能是取出保存在当前fiber的hooks链表中对应的hook节点。

function updateWorkInProgressHook(): Hook {
  // 1. 移动currentHook指针
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {  /* 如果 currentHook = null 证明它是第一个hooks */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else { /* 不是第一个hooks,那么指向下一个 hooks */
    nextCurrentHook = currentHook.next;
  }

  // 2. 移动workInProgressHook指针
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) { //第一次执行hooks
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 渲染时更新: 本节不讨论
  } else {
    currentHook = nextCurrentHook;
    // 3. 克隆currentHook作为新的workInProgressHook.
    // 随后逻辑与mountWorkInProgressHook一致
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null, // 注意next指针是null
    };
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

代码有点长,总结下

  1. 调用updateWorkInProgressHook获取workInProgressHook对象,updateWorkInProgressHook的逻辑很清晰,主要是让currentHook和WIPHook两个指针同时向后移动(图解如上),复用当前currentHook
  2. 链表拼接: 将 hook.queue.pending 拼接到 current.baseQueue
  3. 状态计算
    • update优先级不够: 加入到 baseQueue 中, 等待下一次 render
    • update优先级足够: 状态合并
    • 更新属性

Talk is cheap, show me the picture

执行updateReducer之前

image.png
放大pendingQueue部分
image.png

链表拼接

image.png

状态计算

循环baseState中的update,依次执行update中的action
如果是setAge(100),那么只用更新值
如果是setAge(age=>age+100),就传入上一次state更新的值
image.png

image.png

useEffect/useLayoutEffect

挂载阶段

mount阶段,useEffect执行mountEffect,useEffectLayout执行mountLayoutEffect
二者内部都会调用mountEffectImpl,参数不一样

function mountEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect | PassiveEffect, // fiberFlags
    HookPassive, // hookFlags
    create,
    deps,
  );
}

function mountLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
): void {
  return mountEffectImpl(
    UpdateEffect, // fiberFlags
    HookLayout, // hookFlags
    create,
    deps,
  );
}
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 1. 创建hook
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 2. 设置workInProgress的副作用标记
  currentlyRenderingFiber.flags |= fiberFlags; // fiberFlags 被标记到workInProgress
  // 2. 创建Effect, 挂载到hook.memoizedState上
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags, // hookFlags用于创建effect
    create, //effect中的回调函数
    undefined,
    nextDeps,//依赖
  );
}

mountEffectImpl逻辑:

  1. 创建hook
  2. 设置workInProgress的副作用标记: flags |= fiberFlags
  3. 创建effect(在pushEffect中), 挂载到hook.memoizedState上, 即 hook.memoizedState = effect

创建effect,挂载updateQueue

function pushEffect(tag, create, destroy, deps) {
  // 1. 创建effect对象
  const effect: Effect = {
    tag,
    create,
    destroy,
    deps,
    next: (null: any),
  };
  // 2. 把effect对象添加到环形链表末尾
  let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
  if (componentUpdateQueue === null) { //如果是第一个useEffect
    // 新建 workInProgress.updateQueue 用于挂载effect对象
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
    // updateQueue.lastEffect是一个环形链表
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    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;
    }
  }
  // 3. 返回effect
  return effect;
}

pushEffect逻辑:

  1. 创建effect.
  2. 把effect对象添加到环形链表末尾.
  3. 返回effect.

effect数据结构

export type Effect = {|
  tag: HookFlags, //effect副作用的类型 
  create: () => (() => void) | void, //传入useEffect的函数
  destroy: (() => void) | void,
  deps: Array<mixed> | null, // 依赖
  next: Effect,
|};
export const NoFlags = /*  */ 0b000;
export const HasEffect = /* */ 0b001; // 有副作用, 可以被触发
export const Layout = /*    */ 0b010; // Layout, dom突变后同步触发
export const Passive = /*   */ 0b100; // Passive, dom突变前异步触发

3种flag分别会在commit提价阶段处理对应的useEffect的回调 这里先不展开详细说,后续我们专门写篇文章探究

mount阶段和useState的处理一样,会根据Hook形成Hook链表,之前的demo中只有状态hook,没有副作用的hook
image.png
image.png
如图,mount阶段创建了hooks链表,如果有effect hook,将创建effect list环形链表,effect list会存贮在2个地方:

  • use(Layout)Effect对应hook元素的memoizedState中。
  • fiber.updateQueue中,本次更新的updateQueue,它会在本次更新的commit阶段中被处理。

effect list

作为拓展介绍下effect list, 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffect。

React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。 在 commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,执行每个effect,从而对相应的 DOM 树执行更改。

更新阶段

更新阶段执行 updateEffect => updateEffectImpl

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // 1. 获取当前hook
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  // 2. 分析依赖
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    // 继续使用先前effect.destroy
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 比较依赖是否变化
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 2.1 如果依赖不变, 新建effect(tag不含HookHasEffect)
        pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }
  // 2.2 如果依赖改变, 更改fiber.flag, 新建effect
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

update阶段判断依赖deps,是否变化
如果没有变化就会被打上NoHookEffect标签,最后会在commit阶段跳过改effect的执行。
如果依赖有变化,打上 HookHasEffect | hookEffectTag, 然后在commit阶段,react会通过标签来判断,是否执行当前的 effect 函数。

小结

useEffect做了什么?

  1. useEffect会生成一个effect对象,保存在hook节点的memoizedState中,同时也push到fiber节点的updateQueue里,组成循环链表。

每次render时,都会对比一下cur hook和wip hook里保存的effect的deps有没有改变,如果改变了,那就更新memoizedState为最新的effect,并且把effect的tag打上HasHookEffect的标记,然后push到fiber的updateQueue里。

  1. 在commit阶段,beforeMutation中,对有副作用的fiber,发起一个异步调度。等到layout结束后,这个异步调度的回调开始执行,处理effect的创建和销毁回调。它会先调用effect的destroy,再调用create。(这部分单独更新)

    useMemo

    function mountMemo<T>(
    nextCreate: () => T,
    deps: Array<mixed> | void | null,
    ): T {
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    const nextValue = nextCreate();
    hook.memoizedState = [nextValue, nextDeps];
    return nextValue;
    }
    
    初始化useMemo,同useState、useEffect一样,先创建Hook,然后执行useMemo中的第一个参数,将执行后的数值,和依赖保存在memoizedState中。
    function updateMemo(
    nextCreate,
    deps,
    ) {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
    const prevState = hook.memoizedState; 
    if (prevState !== null) {
     if (nextDeps !== null) {
       const prevDeps = prevState[1]; // 之前保存的 deps 值
       if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
         return prevState[0];
       }
     }
    }
    const nextValue = nextCreate();
    hook.memoizedState = [nextValue, nextDeps];
    return nextValue;
    }
    
    在组件更新过程中 判断两次 deps是否相等,
    如果不相等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState;
    如果相等 证明没有依赖项改变,那么直接获取缓存的值。定制化缓存,存值取值而已。

注意,nextCreate()执行,如果里面引用了useState等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。

useCallback

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

初始化useCallback,同useState、useEffect一样,先创建Hook,然后中将回调函数和依赖保存在memoizedState中。

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

在组件更新过程中 判断两次 deps是否相等,
如果不相等,返回相应的回调和依赖
如果相等,直接返回上次的回调

useRef

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

useRef的初始化很简单,创建Hook,将值保存在current的属性中,然后将ref保存到memoizedState中

function updateRef(initialValue){
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象
所以解释了useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。

useContext(TODO)

https://github.com/7kms/react-illustration-series/blob/master/docs/main/context.md

总结

无论是哪种Hook,在挂载阶段都会创建Hook,不同useXXX将不同信息保存在hook.memoized
中,多个Hook通过next行程链表

在更新过程中,找到当前fiber的hooks链表中对应的hook,进行对应的更新。

Q1: React 如何管理区分Hooks

通过链表来管理Hooks,按照Hooks的执行顺序依次添加到链表中

Q2: 为什么不能在条件语句等中使用Hooks?

主要原因是Hooks链表!
image.png
根据上面篇幅中的源码介绍,这张图也就不难理解了。
挂载阶段,将会形成一个Hooks链表,其中包含StateHook、EffectHook、MemoHook等,每个Hook中存贮的memoizedState信息不一样,如图。

在更新阶段,通过updateWorkInProgress从current memoizedState的Hooks链表中取出对应的hook,如果hook放到了if条件语句中,会导致链表结构被破坏,导致更新操作发生异常。

Q3: useState和useReducer如何在每次渲染时,返回最新的值

每个Hook节点通过循环链表记录所有的更新操作,在update阶段会依次执行update循环链表中的所有更新操作,最终拿到最新的state返回
image.png

References

https://juejin.cn/post/6844904080758800392
https://juejin.cn/post/6954352486312312845#heading-6
https://juejin.cn/post/6844904080758800392#heading-5
https://react.iamkasong.com/hooks/prepare.html
https://github.com/7kms/react-illustration-series/blob/master/docs/main/hook-summary.md
一文吃透Hooks原理