反应钩子如何确定它们的组件?

Posted

技术标签:

【中文标题】反应钩子如何确定它们的组件?【英文标题】:How do react hooks determine the component that they are for? 【发布时间】:2019-05-27 05:50:19 【问题描述】:

我注意到,当我使用 react hooks 时,子组件的状态更改不会重新渲染没有状态更改的父组件。这个代码沙箱可以看到这一点:https://codesandbox.io/s/kmx6nqr4o

由于没有将组件作为参数或绑定上下文传递给钩子,我错误地认为 react 钩子/状态更改只是触发了整个应用程序的重新渲染,比如秘银是如何工作的,以及 React 的 @ 987654322@ 状态:

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

相反,react 钩子似乎知道它们关联到哪个组件,因此,渲染引擎知道只更新单个组件,并且永远不会在其他任何东西上调用 render,这与上面 React 的设计原则文档所说的相反.

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

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

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

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

【问题讨论】:

@Li357 如果这是唯一发生的事情,那么使用codesandbox.io/s/kmx6nqr4o,父级的 vdom 将与 dom 不同,因此它也会被更新,但父级组件根本不会重新渲染 似乎github.com/facebook/react/blob/… 是答案,但不知道它是如何工作的,以及它如何回答提出的两个问题 如果由于任何状态更改而发生整个 vdom 重新渲染,那么父级的 vdom 也会不同(因为它是当前时间,就像子级一样)。所以这和vdom没有任何关系,它与组件状态关联有关。 @RyanC 肯定是跨界的,但是我认为提出问题的方式太宽泛了,因此它关闭了。对于这个问题,我已尽力将问题和问题隔离到特定示例和解决方案中。 我已删除通过将其标记为可能重复而生成的评论,并提供了答案。 【参考方案1】:

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

in depth article I found after writing this answer Hooks FAQ related *** question related blog post by Dan Abramov

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

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

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

第 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$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 内。如果你想探索更多的路径,堆栈跟踪中较高的关键点是 beginWork 和 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 仍然会知道它与 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 的许多细节,只是想看看它如何处理返回与初始调用相同的设置器。

    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 用于在重新渲染时返回相同的设置器。

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

这是dispatchAction的签名:

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

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

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

https://reactjs.org/docs/codebase-overview.html 的光纤调节器部分 https://github.com/acdlite/react-fiber-architecture https://blog.ag-grid.com/index.php/2018/11/29/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/

【讨论】:

这是很棒的信息!您能否详细说明“纤维将指向调用 useState 的同一组件实例,允许 React 在您为其提供新的状态值时排队重新渲染该特定组件。”,因为我找不到react 网站上有很多关于 Fibers 的文档。或者网站上的文档可能不正确。我更新了问题以详细说明文档的说明,并将问题 1 分为 Q1 和 Q2,以确保可以简洁地回答 Q2。到目前为止,您已经很好地回答了 Q1 和 Q3! 我在最后添加了一些额外的资源来解释光纤架构,并试图澄清我的声明。

以上是关于反应钩子如何确定它们的组件?的主要内容,如果未能解决你的问题,请参考以下文章

反应钩子 useCallback 与循环内的参数

如何在反应钩子中创建一个新的 JSON 对象?

如何使用新的反应路由器钩子测试组件?

如何将输入值从子表单组件传递到其父组件的状态以使用反应钩子提交?

如何正确更新反应钩子状态中的数组

如何在反应中限制经常重新渲染的组件