反应钩子如何确定它们用于的组件? [英] How do react hooks determine the component that they are for?

查看:38
本文介绍了反应钩子如何确定它们用于的组件?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

未捕获的错误:子渲染错误在 Child (index.js? [sm]:24)在 renderWithHooks (react-dom.development.js:15108)在 updateFunctionComponent (react-dom.development.js:16925)在开始工作 $1 (react-dom.development.js:18498)在 HTMLUnknownElement.callCallback (react-dom.development.js:347)在 Object.invokeGuardedCallbackDev (react-dom.development.js:397)在 invokeGuardedCallback (react-dom.development.js:454)在开始工作 $1 (react-dom.development.js:23217)在 performUnitOfWork (react-dom.development.js:22208)在 workLoopSync (react-dom.development.js:22185)在 renderRoot (react-dom.development.js:21878)在 runRootCallback (react-dom.development.js:21554)在 eval (react-dom.development.js:11353)在unstable_runWithPriority (scheduler.development.js:643)在 runWithPriority$2 (react-dom.development.js:11305)在flushSyncCallbackQueueImpl (react-dom.development.js:11349)在flushSyncCallbackQueue (react-dom.development.js:11338)在离散更新 $1 (react-dom.development.js:21677)在离散更新(react-dom.development.js:2359)在 dispatchDiscreteEvent (react-dom.development.js:5979)

所以首先我将关注 renderWithHooks.这位于 ReactFiberHooks.如果您想探索更多的路径,堆栈跟踪中较高的关键点是 beginWorkupdateFunctionComponent 函数,它们都在 ReactFiberBeginWork.js 中.

这是最相关的代码:

 currentRenderingFiber = workInProgress;nextCurrentHook = 当前 !== null ?current.memoizedState : null;ReactCurrentDispatcher.current =nextCurrentHook === null?HooksDispatcherOnMount: HooksDispatcherOnUpdate;让孩子 = Component(props, refOrContext);当前RenderingFiber = null;

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

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

第 2 部分 useState 会发生什么?

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

 导出函数 useState(initialState: (() => S) | S) {const dispatcher = resolveDispatcher();返回 dispatcher.useState(initialState);}

这将使我们进入 ReactFiberHooks.这对于组件的初始安装和更新(即重新渲染)的映射不同.

const HooksDispatcherOnMount: Dispatcher = {useReducer: mountReducer,useState: mountState,};const HooksDispatcherOnUpdate: Dispatcher = {useReducer: updateReducer,使用状态:更新状态,};函数 mountState(初始状态:(() => S) |,): [S, Dispatch] {const hook = mountWorkInProgressHook();if (typeof initialState === '函数') {初始状态 = 初始状态();}hook.memoizedState = hook.baseState = initialState;const 队列 = (hook.queue = {最后:空,调度:空,lastRenderedReducer: basicStateReducer,lastRenderedState: (initialState: any),});const 调度:调度,>= (queue.dispatch = (dispatchAction.bind(空值,//Flow 不知道这是非 null,但我们知道.((当前RenderingFiber: any): Fiber),队列,): 任何));返回 [hook.memoizedState, dispatch];}函数 updateState(初始状态:(() => S) |,): [S, Dispatch] {返回 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.

 函数 updateReducer(减速器:(S,A)=>,初始参数:我,初始化?:我=>,):[S,调度]{const hook = updateWorkInProgressHook();const queue = hook.queue;const dispatch: Dispatch= (queue.dispatch: any);返回 [hook.memoizedState, dispatch];}

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

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

这是 调度动作:

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

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

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

I noticed that when I was using react hooks, a child component's state change does not rerender a parent component that had no state change. This is seen by this code sandbox: https://codesandbox.io/s/kmx6nqr4o

Due to the lack of passing the component to the hook as an argument, or as a bind context, I had mistakenly thought that react hooks / state changes simply triggered an entire application rerender, like how mithril works, and what React's Design Principles states:

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

Instead, it seems that react hooks know which component they are associated to, and thus, the rendering engine knows to only update that single component, and never call render on anything else, opposing what React's Design Principles document said above.

  1. How is the association between hook and component done?

  2. How does this association make it so that react knows to only call render on components whose state changed, and not those without? (in the code sandbox, despite child's state changing, the parent element's render is never called)

  3. How does this association still work when you abstract the usage of useState and setState into custom hook functions? (as the code sandbox does with the setInterval hook)

Seems the answers lie somewhere with this trail resolveDispatcher, ReactCurrentOwner, react-reconciler.

解决方案

First of all, if you are looking for a conceptual explanation of how hooks work and how they know what component instance they are tied to then see the following:

The purpose of this question (if I understand the intent of the question correctly) is to get deeper into the actual implementation details of how React knows which component instance to re-render when state changes via a setter returned by the useState hook. Because this is going to delve into React implementation details, it is certain to gradually become less accurate as the React implementation evolves over time. When quoting portions of the React code, I will remove lines that I feel obfuscate the most relevant aspects for answering this question.

The first step in understanding how this works is to find the relevant code within React. I will focus on three main points:

  • the code that executes the rendering logic for a component instance (i.e. for a function component, the code that executes the component's function)
  • the useState code
  • the code triggered by calling the setter returned by useState

Part 1 How does React know the component instance that called useState?

One way to find the React code that executes the rendering logic is to throw an error from the render function. The following modification of the question's CodeSandbox provides an easy way to trigger that error:

This provides us with the following stack trace:

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)

So first I will focus on renderWithHooks. This resides within 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 functions which are both in ReactFiberBeginWork.js.

Here is the most relevant code:

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

currentlyRenderingFiber represents the component instance being rendered. This is how React knows which component instance a useState call is related to. No matter how deeply into custom hooks you call useState, it will still occur within your component's rendering (happening in this line: let children = Component(props, refOrContext);), so React will still know that it is tied to the currentlyRenderingFiber set prior to the rendering.

After setting currentlyRenderingFiber, it also sets the current dispatcher. Notice that the dispatcher is different for the initial mount of a component (HooksDispatcherOnMount) vs. a re-render of the component (HooksDispatcherOnUpdate). We'll come back to this aspect in Part 2.

Part 2 What happens in useState?

In ReactHooks we can find the following:

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

This will get us to the useState function in ReactFiberHooks. This is mapped differently for initial mount of a component vs. an update (i.e. re-render).

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));
}

The important part to notice in the mountState code above is the dispatch variable. That variable is the setter for your state and gets returned from mountState at the end: return [hook.memoizedState, dispatch];. dispatch is just the dispatchAction function (also in ReactFiberHooks.js) with some arguments bound to it including currentlyRenderingFiber and queue. We'll look at how these come into play in Part 3, but notice that queue.dispatch points at this same dispatch function.

useState delegates to updateReducer (also in ReactFiberHooks) for the update (re-render) case. I'm intentionally leaving out many of the details of updateReducer below except to see how it handles returning the same setter as the initial call.

    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];
    }

You can see above that queue.dispatch is used to return the same setter on re-render.

Part 3 What happens when you call the setter returned by useState?

Here is the signature for dispatchAction:

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

Your new state value will be the action. The fiber and work queue will be passed automatically due to the bind call in mountState. The fiber (the same object saved earlier as currentlyRenderingFiber which represents the component instance) will point at the same component instance that called useState allowing React to queue up the re-rendering of that specific component when you give it a new state value.

Some additional resources for understanding the React Fiber Reconciler and what fibers are:

这篇关于反应钩子如何确定它们用于的组件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆