React Hooks 如何确定它们所针对的组件?

How do react hooks determine the component that they are for?

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

由于没有将组件作为参数或绑定上下文传递给钩子,我曾错误地认为反应钩子/状态更改只会触发整个应用程序重新渲染,例如 mithril 的工作原理,以及什么React 的 Design Principles 状态:

React walks the tree recursively and calls render functions of the whole updated tree during a single tick.

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

  1. hook和组件的关联是怎么做到的?

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

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

似乎答案就在这条线索的某个地方resolveDispatcher, ReactCurrentOwner, react-reconciler

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

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

了解其工作原理的第一步是在 React 中找到相关代码。我主要讲三点:

  • 组件实例执行渲染逻辑的代码(即函数组件,执行组件功能的代码)
  • useState代码
  • 通过调用 setter returned 由 useState
  • 触发的代码

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

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

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

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 (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$ (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 (react-dom.development.js:11305)
    at flushSyncCallbackQueueImpl (react-dom.development.js:11349)
    at flushSyncCallbackQueue (react-dom.development.js:11338)
    at discreteUpdates (react-dom.development.js:21677)
    at discreteUpdates (react-dom.development.js:2359)
    at dispatchDiscreteEvent (react-dom.development.js:5979)

所以首先我会关注 renderWithHooks。这位于 ReactFiberHooks. If you want to explore more of the path to this point, the key points higher in the stack trace are the beginWork and updateFunctionComponent 函数中,它们都在 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 仍然会知道它与 [=21] =] 在渲染之前设置。

设置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 变量。该变量是您所在州的 setter,并在末尾从 mountState 得到 returned:return [hook.memoizedState, dispatch];dispatch 只是 dispatchAction 函数(也在 ReactFiberHooks.js 中),绑定了一些参数,包括 currentlyRenderingFiberqueue。我们将在第 3 部分中了解它们如何发挥作用,但请注意 queue.dispatch 指向同一个 dispatch 函数。

useState 代表 updateReducer(也在 ReactFiberHooks 中)进行更新(重新渲染)。我故意省略了下面 updateReducer 的许多细节,除了看看它如何处理 returning 与初始调用相同的 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 用于 return 相同的 setter 重新渲染。

第 3 部分 当您调用由 useState 编写的 setter return 时会发生什么?

这是 dispatchAction 的签名:

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

您的新状态值为 action。由于 mountState 中的 bind 调用,fiber 和工作 queue 将自动通过。 fiber(之前保存为 currentlyRenderingFiber 的同一对象代表组件实例)将指向调用 useState 的同一组件实例,允许 React 排队重新渲染该特定的当你给它一个新的状态值时的组件。

一些用于了解 React Fiber Reconciler 和纤维是什么的额外资源: