renderWithHooks
所有函数组件的触发是在 renderWithHooks 方法中,前面讲过在 fiber 调和过程中,遇到 Function Component 类型的 fiber (函数组件),就会用 updateFunctionComponent 更新 fiber,在 updateFunction Component 内部就会调用 renderWithHooks。
直接上代码:
js
let currentlyRenderingFiber
function renderWithHooks(
current,workInProgress,Component,props){
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null; /* 每一次执行函数组件之前,先清空状态(用于存放 Hooks 列
表) */
workInProgress.updateQueue = null; /* 清空状态(用于存放 effect list) */
ReactCurrentDispatcher.current = current = = = null || current.memoizedState = = = null ?
HooksDispatcherOnMount : HooksDispatcherOnUpdate /* 判断是初始化组件还是更新组件 */
let children = Component(props, secondArg); /* 执行真正的函数组件,所有的 Hooks 将依次执
行。*/
ReactCurrentDispatcher. current = ContextOnlyDispatcher; /* 将 Hooks 变成第一种,防止
Hooks 在函数组件外部调用,调用直接报错。*/
}
我们将逐行分析流程
从入参开始
入参解释:
current
:当前已经渲染到页面上对应的fiber节点(即上一次渲染的fiber节点)。如果是首次渲染,则current
为null
。workInProgress
:当前正在处理的fiber节点(即本次渲染将要提交的fiber节点)。它是current
的替代品(alternate)。Component
:要渲染的函数组件本身。props
:该函数组件的props。
currentlyRenderingFiber = workInProgress;
- 将全局变量
currentlyRenderingFiber
设置为当前正在工作的fiber(workInProgress
)。这个全局变量用于在后续处理Hooks时,标记当前是哪个fiber正在渲染。
workInProgress.memoizedState = null;
- 清空当前fiber的
memoizedState
。在函数组件中,memoizedState
用来存储Hooks链表(每个Hook都对应链表中的一个节点)。每次渲染前先重置,然后在渲染过程中Hooks会重新构建这个链表。
workInProgress.updateQueue = null;
- 清空当前fiber的
updateQueue
。在函数组件中,updateQueue
用于存储副作用(effects),比如useEffect
、useLayoutEffect
等产生的副作用链表。同样,每次渲染前重置,渲染过程中会重新收集副作用。
ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
- 设置当前的Hooks分发器(dispatcher)。这是Hooks能够区分是挂载阶段(mount)还是更新阶段(update)的关键。
- 判断条件:如果
current
为null
(即没有当前fiber,说明是首次渲染)或者current.memoizedState
为null
(可能是某些边界情况,比如热更新后),则使用挂载时使用的分发器HooksDispatcherOnMount
(其中包含的Hook实现会初始化状态),否则使用更新时使用的分发器HooksDispatcherOnUpdate
(其中包含的Hook实现会更新状态)。 - 这样,在函数组件内部调用
useState
等Hook时,就会根据当前是挂载还是更新来执行不同的逻辑。
let children = Component(props, secondArg);
- 调用函数组件
Component
,传入props
和secondArg
(这里secondArg
在代码片段中没有定义,可能是上下文中的第二个参数,比如ref或者额外的参数)。这个调用会执行函数组件内部的逻辑,包括所有的Hooks调用。 - 执行过程中,Hooks会通过当前设置的
ReactCurrentDispatcher.current
来调用对应的挂载或更新逻辑,并构建Hooks链表和副作用链表。 - 返回值
children
是函数组件返回的React元素(通常是JSX)。
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
- 在函数组件执行完毕后,将Hooks分发器设置为
ContextOnlyDispatcher
。这个分发器的作用是:如果在其上下文中调用Hook(例如在函数组件外部调用Hook),则会抛出错误。这样可以防止在错误的地方使用Hook(比如在类组件或普通函数中调用)。
疑问:为什么每次渲染要重新构建这个链表,重新收集副作用
在函数组件中,每一次渲染(包括初始渲染和后续更新)都需要重新执行整个函数组件。
这意味着组件内部的Hooks也会被重新调用。为了确保Hooks能够正确工作并保持状态的一致性,React需要在每次渲染时重新构建Hooks链表并重新收集副作用。以下详细解释原因:
1. 函数组件的特性:无状态(Stateless)与重新执行
- 函数组件本身是无状态的(在React中,状态由Hooks管理),每次渲染都会从头执行整个函数。
- 因此,在每次渲染中,组件内的所有代码都会重新执行,包括Hooks的调用。为了跟踪这些Hooks,React需要构建一个链表,该链表按顺序记录了每个Hook的调用。
2. Hooks链表的必要性
- Hooks的顺序:React要求每次渲染中,Hooks的调用顺序必须一致(不能在条件语句中随意调用Hook)。这是因为React使用调用顺序来关联每次渲染的Hook状态。
- 每次渲染都需要一个全新的链表:因为每次渲染都可能产生不同的状态(例如,某个
useState
可能被更新,或者某个useEffect
的依赖项变化),所以需要为本次渲染构建一个新的链表,以反映当前的Hooks状态和副作用。
3. 副作用的重新收集
- 副作用(如
useEffect
、useLayoutEffect
等)可能在每次渲染时发生变化: - 依赖项变化:副作用的执行时机可能依赖于某些状态或属性,这些值在每次渲染中可能不同。
- 副作用清理:每个副作用可能需要在上一次渲染的副作用执行前进行清理,然后再执行本次的副作用。
- 因此,每次渲染都需要收集新的副作用,以便在适当的时机(如DOM更新后)执行或清理。
4. 性能优化与正确性
- 避免陈旧闭包:通过重新收集副作用,React确保副作用中使用的状态和属性是最新的,而不是上一次渲染的闭包(除非使用依赖项数组进行控制)。
- 条件性副作用:如果重新收集副作用,那么当条件变化时(例如某个条件分支不再执行某个副作用),React可以正确地移除不再需要的副作用。
示例场景
考虑一个简单的计数器组件:
js
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return <button onClick={() => setCount(count + 1)}>Increment</button>;
}
-
初始渲染:
-
- 构建Hooks链表:
[useState, useEffect]
。
- 构建Hooks链表:
-
- 收集副作用:一个
useEffect
依赖count
(初始为0)。
- 收集副作用:一个
-
点击按钮触发更新:
-
- 重新渲染:重新执行
Counter
函数。
- 重新渲染:重新执行
-
- 重新构建Hooks链表:按顺序重新调用
useState
(返回更新后的状态)和useEffect
。
- 重新构建Hooks链表:按顺序重新调用
-
- 重新收集副作用:因为
count
已变化,所以收集一个新的副作用(依赖项为1),并安排执行(同时清理上一次的副作用)。
- 重新收集副作用:因为
总结
最后看下流程图:
这个函数是React执行函数组件的核心函数,它设置了Hooks的运行环境(通过设置当前dispatcher),执行组件函数,并在执行前后进行fiber状态的清理和设置。
这样设计使得Hooks能够正确地与当前渲染的fiber关联,并且区分挂载和更新两种状态。