从源码理解 React Hook 是如何工作的

2022-12-21 20:22:14 浏览数 (1)

大家好,我是前端西瓜哥。

今天我们从源码来理解 React Hook 是如何工作的。

React Hook 是 React 16.8 后新加入的黑魔法,让我们可以 在函数组件内保存内部状态

Hook 的优势:

  1. 比组件更小粒度的复用,之前复用需要用 Mixin 或 高阶组件(HOC,一个能够返回组件的组件)进行封装,前者依赖关系隐式导致难以维护,后者粒度过大、嵌套过深;
  2. 将处理同一个逻辑的业务代码放在一起,让代码可以更好维护。如果是类组件,得放各个生命周期函数中,逻辑会很分散;
  3. 类组件的 class 写法容易写错,一不小心 this 就指向错误,没错就是说事件响应函数你。另外读取值也麻烦,要写很长的 this.state.count
  4. 拥抱函数式编程,这是 React 团队所提倡的编程写法。

一些全局变量

在讲解源码之前,先认识一些 重要的全局变量

currentlyRenderingFiber:正在处理的函数组件对应 fiber。在执行 useState 等 hook 时,需要通过它知道当前 hook 对应哪个 fiber。

workInProgressHook:挂载时正在处理的 hook 对象。我们会沿着 workInProcess.memoizedState 链表一个个往下走,这个 workInProgressHook 就是该链表的指针。

currentHook:旧的 fiber 的 hooks 链表(current.memorizedState)指针。

ReactCurrentDispatcher:全局对象,是一个 hook 调度器对象,其下有 useState、useEffect 等方法,是我们业务代码中 hook 底层调用的方法。ReactCurrentDispatcher 有三种:

  1. ContextOnlyDispatcher:所有方法都会抛出错误,用于防止开发者在调用函数组件的其他时机调用 React Hook;
  2. HooksDispatcherOnMount:挂载阶段用。比如它的 useState 要将初始值保存起来;
  3. HooksDispatcherOnUpdate:更新阶段用。比如它的 useState 会无视传入的初始值,而是从链表中取出值。

renderWithHooks

构建函数实例是在 renderWithHooks 方法中进行的。

主要逻辑为:

  1. workInProgress 赋值给全局变量 currentlyRenderingFiber,之后执行 hook 就能知道是给哪个组件更新状态了;
  2. 选择 hook 调度器:根据是挂载还是更新阶段,ReactCurrentDispatcher 设置为对应 hook 调度器;
  3. 调用函数组件,进行 render。函数组件内部会调用 Hook,并返回 ReactElement;
  4. 重置全局变量,比如 currentlyRenderingFiber 设置回 null;ReactCurrentDispatcher 还原为 ContextOnlyDispatcher,防止在错误时机使用 Hook。
代码语言:javascript复制
function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderLanes
) {
  renderLanes = nextRenderLanes;
  
  // 1. 将 workInProgress 赋值给全局变量 currentlyRenderingFiber
  // 这样我们在调用 Hook 时就能知道对应的 fiber 是谁
  currentlyRenderingFiber = workInProgress;

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;

  // 2. 根据是挂载还是更新阶段,选择对应 hook 调度器
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // 3. 调用函数组件,里面执行各种 React Hook,并返回 ReactElement
  let children = Component(props, secondArg);

  // 4. hook 调度器还原为 ContextOnlyDispatcher
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  // 将一些全局变量进行重置
  renderLanes = NoLanes;
  currentlyRenderingFiber = null;
  currentHook = null;
  workInProgressHook = null;
  didScheduleRenderPhaseUpdate = false;

  // Hook 数量比上次少,对不上,报错
  if (didRenderTooFewHooks) {
    throw new Error(
      'Rendered fewer hooks than expected. This may be caused by an accidental '  
        'early return statement.',
    );
  }

  return children;
}

下面看看在函数组件一些常见 Hook 是如何工作的。

useState

首先讨论 状态 Hook 中最常见的一种:useState。

挂载阶段(状态初始化)

useState 在挂载阶段,调用的是 HooksDispatcherOnMount.useState,也就是 mountState。

  1. 创建新的 hook 空对象,挂到 workInProcess.memorizedState 队列上(mountWorkInProgressHook 方法);
  2. dispatchSetState 绑定对应 fiber 和 queue,方便以后 setState 快速找到相关对象,最后返回状态值和更新状态方法。
代码语言:javascript复制
function mountState(initialState) {
  // 1. 创建一个 hook 对象,并添加到 workInProcess.memoizedState 链表上
  const hook = mountWorkInProgressHook();

  // useState 传入的可能是个函数,要调用一下拿到初始值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }

  hook.memoizedState = hook.baseState = initialState;

  const queue = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  };
  hook.queue = queue;

  // 更新 state 的方法
  const dispatch = queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  );

  // 返回我们经常用的 [state, setState]
  return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook 实现:

代码语言:javascript复制
function mountWorkInProgressHook() {
  // 新的 hook 空对象
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  // 给 memoizedState 链表加节点的逻辑
  // 写过单链表的会比较理解,头节点要特殊处理
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

更新状态操作(setState)

之前 mountState 时,我们返回了一个绑定了 fiber、queue 参数的 dispatchSetState。setState 更新操作调用的正是这个 dispatchSetState。

第一个 setState 在被调用时会立即计算新状态,这是为了 做新旧 state 对比,决定是否更新组件。如果对比发现状态没变,继续计算下一个 setState 的新状态,直到找到为止。如果没找到,就不进行更新。

其后的 setState 则不会计算,等到组件重新 render 再计算。

为对比新旧状态计算出来的状态值,会保存到 update.eagerState,并将 update.hasEagerState 设置为 true,之后更新时通过它来直接拿到计算后的最新值。

dispatchSetState 会拿到对应的 fiber、queue(对应 hook 的 queue)、action(新的状态)。

  1. 创建一个 update 空对象;
  2. 计算出最新状态,放入到 update.egerState。
  3. 对比新旧状态是否相同(使用 Object.is 对比)。相同就不更新了,结束。不相同,进行后续的操作。
  4. 将 update 放到 queue.interleaved 或 concurrentQueues 链表上(.new 和 .old 文件的逻辑差得有点多),之后更新阶段会搬到 queue.pending。
  5. 将当前 fiber 的 lanes 设置为 SyncLane,这样后面的 setState 就不会立刻计算最新状态了,而是在更新阶段才计算。
  6. 接着是调度更新(scheduleUpdateOnFiber),让调度器进行调度,执行更新操作。
代码语言:javascript复制
function dispatchSetState(fiber, queue, action) {
  const lane = requestUpdateLane(fiber);

  // 创建一个 update 更新对象
  const update = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };

  if (isRenderPhaseUpdate(fiber)) {
    // 渲染阶段更新,先不讨论这种特殊情况
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      // 第二次 setState 时,fiber.lanes 为 SyncLane
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState = queue.lastRenderedState;
          // 计算新状态
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          // 对比新旧状态是否不同
          if (is(eagerState, currentState)) {
            // 状态没改变,当前 setState 无效,return 结束,无事发生
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        }
      }
    }

    // 将 update 加到 queue 链表末尾
    // 将 fiber 标记为 SyncLane
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      // 调度 fiber 更新
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

更新阶段(获取最新状态)

我们先了解一个前置知识:useState 是特殊的 useReducer。

useState 本质上在使用 useReducer,在 React 源码层提供了特殊的名为 basicStateReducer 的 reducer,后面源码解析中会看到它。

代码语言:javascript复制
const _useState = (initalVal) => {
 return React.useReducer(
    function (preState, action) {
      // action 对应 setState 传入的最新状态
      // 如果不是函数,直接更新为最新状态
      // 如果是函数,传入 preState 并调用函数,并将返回值作为最新状态
      return typeof action === 'function' ? action(preState) : action;
    },
    initalVal
  )
}

回到正题。

useState 在更新阶段会拿到上一次的状态值,此阶段调用的是 HooksDispatcherOnUpdate.useState,也就是 updateState。

updateState 会调用 updateReducer(useReducer 更新阶段也用这个),这也是为什么我说 setState 是特殊 useReducer 的原因。

updateReducer 主要工作有两个:

  1. 从 current.memorizedState 拷贝 hook 到 workInProcess 下(updateWorkInProgressHook 方法);
  2. 将 hook.queue.pending 队列合并到 currentHook.baseQueue 下,然后遍历队列中的 update 对象,使用 action 和 reducer 计算出最新的状态,更新到 hook 上,最后返回新状态和新 setState。
代码语言:javascript复制
function updateState(initialState) {
  // 实际用的是 updateReducer
  return updateReducer(basicStateReducer);
}

// reducer 函数
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

// setReducer 更新阶段对应的 updateReducer
function updateReducer(reducer, initialArg, init) {
  // ----- 【1】 拷贝 hook(current -> workInProcess),并返回这个 hook -----
  const hook = updateWorkInProgressHook();
  
  // ----- 【2】 读取队列,计算出最新状态,更新 hook 的状态 -----
  // ...
}

先看看 updateWorkInProgressHook 方法。

该方法中,currentHook 设置为 current.memoizedState 链表的下一个 hook,拷贝它到 currentlyRenderingFiber.memoizedState 链表上,返回这个 hook。

代码语言:javascript复制
function updateWorkInProgressHook() {
  // 1. 移动 currentHook 指针
  //(来自 current.memoizedState 链表)
  var nextCurrentHook; 
  if (currentHook === null) {
    var current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }

  // 2. 移动 workInProgressHook 指针
  //(来自 currentlyRenderingFiber.memoizedState 链表)
  var nextWorkInProgressHook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // 这种情况为 “渲染时更新逻辑”(在 render 时调用了 setState)
    // 为了更聚焦普通情况,这里不讨论
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    // 3. 渲染时不更新,nextWorkInProgressHook 就一定是 null
    if (nextCurrentHook === null) {
      throw new Error('Rendered more hooks than during the previous render.');
    }

    currentHook = nextCurrentHook;
    var newHook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null // next 就不拷贝了
    };

    // 4. 经典单链表末尾加节点写法
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }

  // 5. 返回拷贝 hook 对象
  return workInProgressHook;
}

拿到拷贝后的 hook,就可以计算新状态值了。

首先将 hook.queue.pending 队列合并到 currentHook.baseQueue 下。该队列包含了一系列 update 对象(因为可能调用了多次 setState),里面保存有 setState 传入的最新状态值(函数或其他值)。

然后遍历 update 计算出最新状态,保存回 hook,并返回最新状态值和 setState 方法。

代码语言:javascript复制
function updateReducer(reducer, initialArg, init) {
  // ----- 【1】 拷贝 hook(current -> workInProcess),并返回这个 hook ----
  const hook = updateWorkInProgressHook();
  
  // ----- 【2】 读取队列,计算出最新状态,更新 hook 的状态 -----
  // 取出 hook.queue 链表,添加到 current.baseQueue 末尾
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = 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;
  }

  // 处理更新队列
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;

    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
  
    // 循环,根据 baseQueue 链表下的 update 对象计算新状态
    do {   
      // 删掉了一些跳过更新的逻辑

      if (update.hasEagerState) {
        // 为了对比新旧状态来决定是否更新,所计算的新状态。
        // 如果不同,给 update.hasEagerState 设置为 true
        // 新状态赋值给 update.eagerState
        newState = update.eagerState;
      } else {
        // 计算新状态
        const action = update.action;
        newState = reducer(newState, action);
      }
      
      update = update.next;
    } while (update !== null && update !== first);

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

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

  // 更新 hook 状态
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }

  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

useEffect

有些逻辑类似 useState,比如创建 hook 的 mountWorkInProgressHook 方法实现,所以一些重复逻辑就不说了,直奔核心。

挂载阶段

核心函数是 mountEffectImpl。

  1. 【mountWorkInProgressHook】创建一个 hook 空对象,放到 workInProcess.memorizedState 下;
  2. 【pushEffect】创建 effect,添加到 当前 fiber 的 updateQueue 的链表上,并将该 effect 赋值给 hook.memoizedState。
代码语言:javascript复制
mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  // create 和 deps 是 useEffect 接受的两个参数
  
  // 1. 新建 hook 对象
  const hook = mountWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;
  currentlyRenderingFiber.flags |= fiberFlags;
  
  // 2. 新建 effect 对象,放到 hook.memoizedState 下。
  hook.memoizedState = pushEffect(
    HookHasEffect | hookFlags,
    create,
    undefined,
    nextDeps,
  );
}

pushEffect 实现:

代码语言:javascript复制
function pushEffect(tag, create, destroy, deps) {
  // 创建 effect
  var effect = {
    tag: tag,
    create: create,
    destroy: destroy,
    deps: deps,
    next: null
  };
  var componentUpdateQueue = currentlyRenderingFiber.updateQueue;

  // 添加到当前 fiber.updateQueue 下。
  // updateQueue.laseEffect 保存链表的最后一个 effect
  // 且使用的是环形链表,通过 updateQueue.laseEffect.next 得到链表头节点
  
  // 如果 updateQueue 为 null,初始化一个空的 updateQueue 对象
  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber.updateQueue = componentUpdateQueue;
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    // 往 updateQueue.lastEffect 链表上添加 effect 对象。
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

更新阶段

核心实现在 updateEffectImpl。

  1. 从 current 拷贝 hook 到 workInProcess;
  2. 对比新旧依赖项 deps,如果没改变,也创建 effect 加队列上(但最终不会执行),结束;否则继续;
  3. 给当前 fiber 打上 PassiveEffect,表示有 useEffect 的回调要执行;
  4. 创建 effect ,tag 补上加 HookHasEffect,然后加队列上,后面会执行。
代码语言:javascript复制
function updateEffect(create, dep) {
  return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}

function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
  // hookFlags 此时为 PassiveEffect(代表)
  
  // 1. 从 current 拷贝 hook 到 workInProcess
  const hook = updateWorkInProgressHook();

  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;

  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    // 存在依赖项
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      // 依赖项没有改变,结束
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 还是会新建 effect,更新 updateQueue 和 memorizedState
        // 但 tag 只是 PassiveEffect,后面遍历时不会执行
        hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
        return;
      }
    }
  }

 // 当前 fiber 打上 PassiveEffect 标记
  // 该标记表示存在需要执行的 useEffect
  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushEffect(
    // 相比上面依赖项不变的情况,这里加了 HookHasEffect 标签
    // 之后根据 fiber.updateQueue 会执行
    HookHasEffect | hookFlags,
    create,
    destroy,
    nextDeps,
  );
}

我们看下依赖项对比算法 areHookInputsEqual 的细节,它同时遍历到新旧依赖项最长的尾部,进行 Object.is 对比。在空数组情况下,这个比较一定返回 true,所以能模拟 componentDidMount / Unmount 的效果。

代码语言:javascript复制
function areHookInputsEqual(nextDeps, prevDeps) {
  for (let i = 0; i < prevDeps.length && i < nextDeps.length; i  ) {
    if (is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

useEffect 的 create 和 destroy 的执行时机

当 commit 阶段结束后,useEffect 的 create 和 destroy 会被 Schedule 调度器异步调度执行。

fiber.updateQueue 下的 effect 会按顺序取出,然后一个个执行。

代码语言:javascript复制
function commitPassiveUnmountOnFiber(finishedWork) {
 // 执行所有 tag 为 HookPassive | HookHasEffect 的 effect 的 destroy
  commitHookEffectListUnmount(
    HookPassive | HookHasEffect,
    finishedWork,
    finishedWork.return,
  );
  // 执行所有 tag 为 HookPassive | HookHasEffect 的 effect 的 create
  commitHookEffectListUnmount(
    HookPassive | HookHasEffect,
    finishedWork,
    finishedWork.return,
  );
}

之前依赖项相同的话,虽然也创建 effect,但它的 tag 对不上,是不会执行的。

一些面试题的简单回答

1、React Hooks 为什么不能写在条件语句中?

我们要保证 React Hooks 的顺序一致。

函数组件的状态是保存在 fiber.memorizedState 中的。它是一个链表,保存调用 Hook 生成的 hook 对象,这些对象保存着状态值。当更新时,我们每调用一个 Hook,其实就是从 fiber.memorizedState 链表中读取下一个 hook,取出它的状态。

如果顺序不一致了或者数量不一致了,就会导致错误,取出了一个其他 Hook 对应的状态值。

2、React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?

Hooks 底层调用的是一个全局变量 ReactCurrentDispatcher 的一系列方法。

这个全局变量会在不同阶段设置为不同的对象。render 过程中,挂载阶段设置为 HooksDispatcherOnMount,更新阶段设置为 HooksDispatcherOnUpdate。它们会读取 currentlyRenderingFiber 全局变量,这个全局变量代表正在处理的 fiber,读取它进行一些设置状态和读取状态等操作。

在 render 阶段外,会设置为 ContextOnlyDispatcher,这个对象下所有方法都会抛出错误,因为此时不存在正常处理的 fiber,使用时机是并不对。

结尾

本文只讲了 状态 Hook 代表 useState,和 副作用 Hook 代表 useEffect,其他 Hook 其实也差不多。

我是前端西瓜哥,欢迎关注我,学习更多知识。


0 人点赞