likes
comments
collection
share

React:通俗易懂的 hooks 链表

作者站长头像
站长
· 阅读数 13

前言

如今的 React 几乎是函数式组件 + hooks 的天下。我们知道,hooks 只能使用在函数式组件中,那么当组件挂载运行时,hooks 又是如何运行的呢?数据又是如何保存的呢?本篇文章将揭示 hooks 的原理。

函数组件与 hooks 的关系

renderWithHooks

在 React 源码中,当函数式组件初始化时,会调用 renderWithHooks 函数,我们来看看源码(这里只截取一部分与hooks有关的,感兴趣的朋友可以去看 React 源码)

//react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks(
  current,        // currentFiber
  workInProgress, // workInProgressFiber
  Component,      // 组件
  props,          // 组件 props
  secondArg,      // 
  nextRenderLanes,// Scheduler 优先级相关
) {
  renderLanes = nextRenderLanes;
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null; // 存储 hooks 的值
  workInProgress.updateQueue = null;   // 与 effect 副作用相关的更新队列
  
  //...

  // 区分是 初始化 还是 更新
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

  // ...
  
  // 调用函数组件
  let children = Component(props, secondArg);
  
  // ...
  
  return children
  
}

执行上面的代码时:

  • 根据 current(currentFiber) 判断当前组件是否是初始化
  • 是初始化,走 HooksDispatcherOnMount
  • 是后续更新,走 HooksDispatcherOnUpdate

那上面的 HooksDispatcherOnMount、HooksDispatcherOnUpdate 又是个啥?

继续看源码:

//react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {//mount 时
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  //...
};

const HooksDispatcherOnUpdate: Dispatcher = {//update 时
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  //...
};

// Hook 对象
export type Hook = {
  memoizedState: any, // hook 对象的值,useState 保存 state , useEffect 保存 effect 链表,useRef 保存 { current: xxx } ....
  baseState: any,  // 初始 state
  baseQueue: Update<any, any> | null, // 初始队列
  queue: any, // 需要更新的队列,比如多次 setState
  next: Hook | null, // 下一个 hook 节点
};

从上面的代码我们不妨大胆总结:在函数式组件挂载时,hooks 都会走 mountXXX,更新时,hooks 会走 updateXXX。

那这个结论有什么用呢?我们继续往下看。

hooks Mount阶段

mountWorkInProgressHook

当组件初始化的时候,会调用 mountXXX 的 hooks,而他们内部都会调用 mountWorkInProgressHook 的回调函数:

React:通俗易懂的 hooks 链表

React:通俗易懂的 hooks 链表

React:通俗易懂的 hooks 链表

那这个 mountWorkInProgressHook 干了什么?看下面的代码:

React:通俗易懂的 hooks 链表

也就是说,组件初始化时,按顺序调用 hooks 时,内部会通过 mountWorkInProgressHook() 函数创建 hook 节点,组装一个链表,最终挂载到 Fiber 节点的 memoizedState 字段上。即:hook 链表会存储到 Fiber 的 memoizedState 属性上。

举个例子:

const App = () => {
    const [state, setState] = useState(0);
    const ref = useRef(1);
    
    useEffect(() => {
        setTimeOut(() => {
            setState(2);
        }, 500);
    }, [])
}

我们依次使用了 useState、useRef、useEffect 三个 hooks,那么就会形成下面这种结构的链表:

  1. 调用 useState:
workInProgress.memoizedState: useState
                                 ^
                        workInProgressHook
  1. 调用 useRef:
workInProgress.memoizedState: useState -> useRef
                                            ^
                                    workInProgressHook
  1. 调用 useEffect:
workInProgress.memoizedState: useState -> useRef -> useEffect
                                                        ^
                                               workInProgressHook

用图来描述的话就是:

React:通俗易懂的 hooks 链表

总结: hook 每次执行时,都会通过 mountWorkInProgress 创建对应的 hook 节点,将这些 hook 节点组成链表,赋值给 workInProgress。

注意: 不同的 hook,memoizedState 对应的值不同:(Fiber 上的 memoizedState 指向 hooks 链表,hook 身上的 memoizedState 存储他们自己的状态,二者不一样!

  • useState:memoizedState 等于 state 的值
  • useEffect:memoizedState 等于 effect 链表,effect 链表又存储在 fiber.updateQueue 上,每个 effect 相当于 { create: callback, dep: dep } 这样的对象
  • useRef:memoizedState 等于 { current: initialValue }
  • useMemo:memoizedState 等于 [callback(), dep]
  • useCallback:memoizedState 等于 [callback, dep]
  • useReducer:memoizedState 等于state 的值

为什么不能在条件、循环里面使用 hook

在上述例子中,我们把 useRef 放入条件语句中

let ref = null;
let isFirst = true;
if (isFirst) {
    curRef = useRef(1);
    //初始化后将条件改为 false
    isFirst = false;
}

后续组件重新 render 时,if 判断进不去,会发生下面的情况:

React:通俗易懂的 hooks 链表

上面的图解看出:一旦在条件语句中声明hooks,函数组件更新时,hooks 链表结构被破坏,currentFiber树memoizedState 缓存 hooks链表 的信息,和 workInProgress 不一致,如果涉及到读取state等操作,就会发生异常。因此不能在条件、循环语句中使用 hooks。

hooks Update阶段

前面我们说了,mount 的时候 hooks 调用 mountXXX,update 的时候调用 updateXXX。而在 update 阶段,会涉及到一个函数叫做 updateWorkInProgressHook

updateWorkInProgressHook

首先,来通读一遍源码:我们将源码分成 三个部分

// 首先,在组件中使用的多个 hook 会以链表的形式,存储在 fiber 节点的 memoizedState 字段上,
// currentHook 变量表示 currentFiber 树(当前渲染到页面)上的 旧的 hooks 链表。
// workInProgressHook 变量表示 一颗正在构建的新的 hooks 链表,它将会在构建成功后,
// 挂载到 workInProgressFiber 树上。
// currentlyRenderingFiber 变量就是 workInProgressFiber 树。
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
let currentlyRenderingFiber: Fiber = (null: any);

function updateWorkInProgressHook(): Hook {
  /** 第一部分:
      nextCurrentHook 中间变量,获取旧的 hooks 链表 
  **/
  let nextCurrentHook: null | Hook;
  // 如果 curentHook 为空
  if (currentHook === null) {
    // 通过 alternate 属性,找到对应的旧的 currentFiber 树
    const current = currentlyRenderingFiber.alternate;
    // 如果旧的 currentFiber 不为空,则取 memoizedState 拿到旧的 hooks 链表
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      // 否则为 null
      nextCurrentHook = null;
    }
  } else {
    // 如果 currentHook 不为空,则遍历下一个 hook 节点
    nextCurrentHook = currentHook.next;
  }

  /** 第二部分:
      nextWorkInProgressHook 中间变量,先复用一份旧的 hooks 链表形成新的 hooks 链表
  **/
  let nextWorkInProgressHook: null | Hook;
  // 如果 workInProgressHook 为空,则表示还没指向 新的 hooks 地址。
  if (workInProgressHook === null) {
    // 这里同样通过 memoizedState 先拿到 currentFiber 的旧 hooks 链表。
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    // 不是头节点,遍历下一个 hook 节点
    nextWorkInProgressHook = workInProgressHook.next;
  }

  /** 第三部分: 
      最终由 workInProgressHook 指向新的 hooks 链表
      最终由 currentHook 指向旧的 hooks 链表
  **/
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    currentHook = nextCurrentHook;

    // 克隆旧的 hook 节点
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    if (workInProgressHook === null) {
      // 头节点,挂载到 workInProgressFiber 树上
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // 在链表后添加新节点
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

其实上面的代码可以概括为:

  • 第一部分:由 nextCurrentHook 中间变量 记录旧的 hooks 链表
  • 第二部分:由 nextWorkInProgressHook 中间变量 克隆旧的 hook 节点形成新的 hooks 链表。
  • 第三部分:currentHook 指向旧 hooks链表;workInProgressHook 指向新的 hooks 链表,返回 workInProgressHook

之所以要克隆旧 hook,是为了能保留旧 hook 的状态,提供新旧 hook 对比的能力。

那其实上面的代码可以用下面的流程图解释:

React:通俗易懂的 hooks 链表

克隆完毕后:

React:通俗易懂的 hooks 链表

updateRef、updateMemo、updateCallback

这几个 hook 的代码比较简单,直接看代码

updateRef

React:通俗易懂的 hooks 链表

updateRef 函数调用时,始终返回的是最开始的那个对象。因为 ref.current 的值在整个生命周期不变。

updateMemo

React:通俗易懂的 hooks 链表

updateMemo 时,内部会通过 areHookInputsEqual(nextDeps, prevDeps) 判断依赖是否发生改变。没改变就返回 prevState[0], 改变了就重新执行回调函数 nextCreate(),更新 hook 节点的 memoizedState,返回记忆值。

updateCallback

和 updateMemo 类似:

React:通俗易懂的 hooks 链表

updateCallback 时,内部会通过 areHookInputsEqual(nextDeps, prevDeps) 判断依赖是否发生改变。没改变就返回 prevState[0], 改变了就 更新 hook 节点的 memoizedState,返回记忆值函数。

updateState、updateReducer

因为 updateState 会调用 updateReducer,所以放在一起来讲。 我们先来看 mountState 时做了什么

function mountState() {
  // 创建 hook
  const hook = mountWorkInProgressHook();
  
  // baseState 保存初始值
  hook.memoizedState = hook.baseState = initialState;
  
  // 创建更新队列
  const queue = {
    pending, //要更新的队列
    lanes, // 优先级
    dispatch, // 也就是 setState()
    lastRenderedReducer,
    lastRenderedState: () => any,
  };
  
  // 绑定 dispatch
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  )));
  // 6. 返回 state 和 dispatch
  return [hook.memoizedState, dispatch];
}

流程图如下:

React:通俗易懂的 hooks 链表

那么当 updateReducer 时

// updateReducer 部分代码
function updateReducer() {
  const queue = hook.queue;
  let baseQueue = hook.baseQueue;
  const pendingQueue = queue.pending;
  
  if (pendingQueue !== null) {
    // 如果 queue.pending 不为空,表示有尚未处理的更新
    // 把它们合并到 baseQueue 中,是个单项环形链表
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue;
    // 清理 queue.pending 待更新队列
    queue.pending = null;
  }
  
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = hook.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast= null;
    let update = first;
    do {
      // 更新优先级相关
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        const clone = {
          lane: updateLane,
          revertLane: update.revertLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: null,
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        const revertLane = update.revertLane;
        if (!enableAsyncActions || revertLane === NoLane) {
          if (newBaseQueueLast !== null) {
            const clone = {
              lane: NoLane,
              revertLane: NoLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: null,
            };
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }
        } else {
          if (isSubsetOfLanes(renderLanes, revertLane)) {
            update = update.next;
            continue;
          } else {
            const clone = {
              lane: NoLane,
              revertLane: update.revertLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: null,
            };
            if (newBaseQueueLast === null) {
              newBaseQueueFirst = newBaseQueueLast = clone;
              newBaseState = newState;
            } else {
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
            currentlyRenderingFiber.lanes = mergeLanes(
              currentlyRenderingFiber.lanes,
              revertLane,
            );
            markSkippedUpdateLanes(revertLane);
          }
        }

        const action = update.action;
        if (shouldDoubleInvokeUserFnsInHooksDEV) {
          reducer(newState, action);
        }
        if (update.hasEagerState) {
          newState = ((update.eagerState: any): S);
        } else {
          // 计算得到新的 state
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  
  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

上面的代码很复杂,但可以概括为:

  • 将 pendingQueue 合并到 queue React:通俗易懂的 hooks 链表
  • 根据优先级 计算最新的 state 并返回

updateEffect

同样,先看看 mountEffect 时干了个什么

function mountEffectImpl(
  fiberFlags,
  hookFlags,
  create,
  deps,
) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  // 调用了 pushEffect 
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    createEffectInstance(),
    nextDeps,
  );
}

// pushEffect 部分代码
function pushEffect(
  tag,
  create,
  inst,
  deps,
) {
  const effect: Effect = {
    tag,
    create,
    inst,
    deps,
    next,
  };
  // 把创建的 effect 对象放到 fiber.updateQueue 上
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
  
  //其余代码...
  
  // lastEffect 
  componentUpdateQueue.lastEffect = effect.next = effect;
  
  return effect;
}

mountEffect干了两件事:

  • 创建 effect 对象,记录了依赖数组、回调函数、清理函数、处理方式
  • 挂载到 fiber 树的 updateQueue 属性上,updateQueue 是个链表,链表元素就是 effect 对象

如下图:

React:通俗易懂的 hooks 链表

然后,updateEffect 时:

function updateEffectImpl(
  fiberFlags,
  hookFlags,
  create,
  deps,
) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect = hook.memoizedState;
  const inst = effect.inst;

  // currentHook is null when rerendering after a render phase state update.
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

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

updateEffect 的事情很简单:

  • 通过 areHookInputsEqual 判断 deps 是否相等
  • 若相等,则不需要执行更新,effect tag 值是 NoHookEffect
  • 若不相等,则更新 effect,effect tag 值为 hookEffectTag

effect 对象的其他属性:

  • create 属性:useEffect 的第一个入参函数
  • destroy 属性:上一次 Effect 的 destroy
  • deps 属性:依赖数组

React:通俗易懂的 hooks 链表

那既然知道了 effect 对象哪些更新哪些不更新,那什么时候执行 effect 呢?

effect 的执行会在 Fiber 的 commit 阶段,而入口是 commitRootImpl 函数,异步处理 effectList(是个单向循环链表),然后走 commitHookEffectListMount 函数

// commitHookEffectListMount 部分代码
function commitHookEffectListMount(flags, finishedWork) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;

  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
    
        const inst = effect.inst;
        const destroy = create();
        inst.destroy = destroy;

      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

上面的流程是:获取 updateQueue,从 lastEffect.next 指向的最后一个 effect, 开始循环处理

也就是说,在 render 阶段,useEffect 会把对应的 effect 放到 fiber.updateQueue 上,形成单项环形链表,然后在 commit 阶段,遍历 updateQueue,取出 effect 异步执行。

总结

mount 阶段时,hooks 走 mountXXX,内部会调用 mountWorkInProgressHook, 函数创建 hook 节点,组装一个链表,最终挂载到 Fiber 节点的 memoizedState 字段上

update 阶段,hooks 走 updateXXX,内部会调用 updateWorkInProgressHook,克隆 hook 节点,然后拿到对应 hooks 的 hook 节点,然后根据每个 hook 内部具体的逻辑执行更新操作。

结语

以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论