react-hooks如何确定它们用于的组件?

IT技术 reactjs react-hooks
2021-04-10 04:08:35

我注意到当我使用 React 钩子时,子组件的状态更改不会重新呈现没有状态更改的父组件。这是通过这个代码沙箱看到的:https : //codesandbox.io/s/kmx6nqr4o

由于没有将组件作为参数或绑定上下文传递给钩子,我错误地认为react-hooks/状态更改只是触发了整个应用程序的重新渲染,例如 mithril 的工作原理,以及 React 的设计原则声明:

React 递归遍历树并在单个滴答期间调用整个更新树的渲染函数。

相反,react hooks 似乎知道它们关联到哪个组件,因此,渲染引擎知道只更新那个单个组件,而从不调用render其他任何东西,这与上面 React 的设计原则文档所说的相反。

  1. hook和component的关联是怎么做的?

  2. 这种关联是如何实现的,以便 React 知道只调用render状态发生变化的组件,而不调用状态发生变化的组件?(在代码沙箱中,尽管孩子的状态发生了变化,但render永远不会调用父元素的状态

  3. 当您将 useState 和 setState 的用法抽象为自定义钩子函数时,这种关联如何仍然有效?(就像代码沙箱对setInterval钩子所做的那样

似乎答案就在这个线索的某个地方resolveDispatcherReactCurrentOwnerreact-reconciler

1个回答

首先,如果您正在寻找有关钩子如何工作以及它们如何知道它们绑定到哪个组件实例的概念性解释,请查看以下内容:

这个问题的目的(如果我正确理解了问题的意图)是深入了解实际的实现细节,即 React 如何通过useState钩子返回的 setter 知道当状态发生变化时要重新渲染哪个组件实例因为这将深入研究 React 实现细节,所以随着 React 实现的发展,它肯定会逐渐变得不那么准确。在引用 React 代码的部分内容时,我将删除我认为混淆了回答这个问题最相关方面的行。

理解其工作原理的第一步是在 React 中找到相关代码。我将重点关注三个要点:

  • 为组件实例执行渲染逻辑的代码(即对于功能组件,执行组件功能的代码)
  • useState代码
  • 通过调用返回的 setter 触发的代码 useState

第 1 部分React 如何知道调用 的组件实例useState

查找执行渲染逻辑的 React 代码的一种方法是从渲染函数中抛出错误。问题的 CodeSandbox 的以下修改提供了触发该错误的简单方法:

编辑 React 挂钩父状态与子状态

这为我们提供了以下堆栈跟踪:

Uncaught Error: Error in child render
    at Child (index.js? [sm]:24)
    at renderWithHooks (react-dom.development.js:15108)
    at updateFunctionComponent (react-dom.development.js:16925)
    at beginWork$1 (react-dom.development.js:18498)
    at HTMLUnknownElement.callCallback (react-dom.development.js:347)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:397)
    at invokeGuardedCallback (react-dom.development.js:454)
    at beginWork$$1 (react-dom.development.js:23217)
    at performUnitOfWork (react-dom.development.js:22208)
    at workLoopSync (react-dom.development.js:22185)
    at renderRoot (react-dom.development.js:21878)
    at runRootCallback (react-dom.development.js:21554)
    at eval (react-dom.development.js:11353)
    at unstable_runWithPriority (scheduler.development.js:643)
    at runWithPriority$2 (react-dom.development.js:11305)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11349)
    at flushSyncCallbackQueue (react-dom.development.js:11338)
    at discreteUpdates$1 (react-dom.development.js:21677)
    at discreteUpdates (react-dom.development.js:2359)
    at dispatchDiscreteEvent (react-dom.development.js:5979)

所以首先我将重点放在renderWithHooks. 这驻留在ReactFiberHooks 中如果你想探索更多的路径到这一点,堆栈跟踪中的关键点是beginWorkupdateFunctionComponent函数,它们都在 ReactFiberBeginWork.js 中。

这是最相关的代码:

    currentlyRenderingFiber = workInProgress;
    nextCurrentHook = current !== null ? current.memoizedState : null;
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
    let children = Component(props, refOrContext);
    currentlyRenderingFiber = null;

currentlyRenderingFiber表示正在呈现的组件实例。这就是 React 知道useState调用与哪个组件实例相关的方式。无论你调用的自定义钩子有多深useState,它仍然会出现在你的组件渲染中(发生在这一行let children = Component(props, refOrContext);:),所以 React 仍然会currentlyRenderingFiber在渲染之前知道它与集合相关联

设置后currentlyRenderingFiber,它还设置了当前的调度程序。请注意,对于组件 ( HooksDispatcherOnMount)的初始安装和组件的重新渲染( ) ,调度程序是不同的HooksDispatcherOnUpdate我们将在第 2 部分中回到这个方面。

第 2 部分发生了useState什么?

ReactHooks 中,我们可以找到以下内容:

    export function useState<S>(initialState: (() => S) | S) {
      const dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
    }

这将使我们进入ReactFiberHooks 中useState函数这对于组件的初始安装与更新(即重新渲染)的映射不同。

const HooksDispatcherOnMount: Dispatcher = {
  useReducer: mountReducer,
  useState: mountState,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  useReducer: updateReducer,
  useState: updateState,
};

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

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

mountState上面代码中需要注意的重要部分dispatch变量。该变量是您的状态的设置器,并mountState在最后返回return [hook.memoizedState, dispatch];dispatch只是一个dispatchAction函数(也在 ReactFiberHooks.js 中),带有一些绑定到它的参数,包括currentlyRenderingFiberqueue我们将在第 3 部分中了解这些是如何发挥作用的,但请注意它们queue.dispatch指向的是同一个dispatch函数。

useState委托updateReducer(也在ReactFiberHooks 中)更新(重新渲染)的情况。我有意省略了updateReducer下面的许多细节,只是为了看看它如何处理返回与初始调用相同的 setter。

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      return [hook.memoizedState, dispatch];
    }

您可以在上面看到queue.dispatch用于在重新渲染时返回相同的 setter。

第 3 部分当您调用 返回的 setter 时会发生什么useState

这是dispatchAction的签名

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A)

您的新状态值将是action. fiber工作queue将自动由于传递bind呼叫mountStatefiber(同一个对象作为较早保存currentlyRenderingFiber它代表的组件实例)将在同一组件实例指出,呼吁useState让react过来的时候你给它一个新的状态值排队特定组件的重新渲染。

一些用于理解 React Fiber Reconciler 以及什么是 Fiber 的额外资源:

我在最后添加了一些额外的资源来解释光纤架构,并试图稍微澄清我的陈述。
2021-05-25 04:08:35
这是一个很好的信息!您能否详细说明“光纤将指向调用 useState 的同一个组件实例,从而允许 React 在您给它一个新的状态值时将该特定组件的重新渲染排队等候。”,因为我找不到react 网站上有那么多关于 Fibers 的文档。或者网站上的文档可能不正确。我更新了问题以详细说明文档说明的内容,并将问题 1 分为 Q1 和 Q2,以确保 Q2 可以得到简洁的回答。到目前为止,您已经很好地回答了 Q1 和 Q3!
2021-05-28 04:08:35