反应钩子如何确定它们用于的组件? [英] How do react hooks determine the component that they are for?
问题描述
我注意到,当我使用 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.如果您想探索更多的路径,堆栈跟踪中较高的关键点是 beginWork 和 updateFunctionComponent 函数,它们都在 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 中),绑定了一些参数,包括 currentlyRenderingFiber
和 queue
.我们将在第 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 是什么的额外资源:
- 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/
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.
How is the association between hook and component done?
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'srender
is never called)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:
- in depth article I found after writing this answer
- Hooks FAQ
- related StackOverflow question
- related blog post by Dan Abramov
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:
- Fiber Reconciler portion of 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/
这篇关于反应钩子如何确定它们用于的组件?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!