《深入浅出react》总结之 11. 2. 2 renderWithHooks 执行函数

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节点)。如果是首次渲染,则currentnull
  • workInProgress:当前正在处理的fiber节点(即本次渲染将要提交的fiber节点)。它是current的替代品(alternate)。
  • Component:要渲染的函数组件本身。
  • props:该函数组件的props。
  1. currentlyRenderingFiber = workInProgress;
  • 将全局变量currentlyRenderingFiber设置为当前正在工作的fiber(workInProgress)。这个全局变量用于在后续处理Hooks时,标记当前是哪个fiber正在渲染。
  1. workInProgress.memoizedState = null;
  • 清空当前fiber的memoizedState。在函数组件中,memoizedState用来存储Hooks链表(每个Hook都对应链表中的一个节点)。每次渲染前先重置,然后在渲染过程中Hooks会重新构建这个链表。
  1. workInProgress.updateQueue = null;
  • 清空当前fiber的updateQueue。在函数组件中,updateQueue用于存储副作用(effects),比如useEffectuseLayoutEffect等产生的副作用链表。同样,每次渲染前重置,渲染过程中会重新收集副作用。
  1. ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;
  • 设置当前的Hooks分发器(dispatcher)。这是Hooks能够区分是挂载阶段(mount)还是更新阶段(update)的关键。
  • 判断条件:如果currentnull(即没有当前fiber,说明是首次渲染)或者current.memoizedStatenull(可能是某些边界情况,比如热更新后),则使用挂载时使用的分发器HooksDispatcherOnMount(其中包含的Hook实现会初始化状态),否则使用更新时使用的分发器HooksDispatcherOnUpdate(其中包含的Hook实现会更新状态)。
  • 这样,在函数组件内部调用useState等Hook时,就会根据当前是挂载还是更新来执行不同的逻辑。
  1. let children = Component(props, secondArg);
  • 调用函数组件Component,传入propssecondArg(这里secondArg在代码片段中没有定义,可能是上下文中的第二个参数,比如ref或者额外的参数)。这个调用会执行函数组件内部的逻辑,包括所有的Hooks调用。
  • 执行过程中,Hooks会通过当前设置的ReactCurrentDispatcher.current来调用对应的挂载或更新逻辑,并构建Hooks链表和副作用链表。
  • 返回值children是函数组件返回的React元素(通常是JSX)。
  1. 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. 副作用的重新收集

  • 副作用(如useEffectuseLayoutEffect等)可能在每次渲染时发生变化:
  • 依赖项变化:副作用的执行时机可能依赖于某些状态或属性,这些值在每次渲染中可能不同。
  • 副作用清理:每个副作用可能需要在上一次渲染的副作用执行前进行清理,然后再执行本次的副作用。
  • 因此,每次渲染都需要收集新的副作用,以便在适当的时机(如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]
    • 收集副作用:一个useEffect依赖count(初始为0)。
  • 点击按钮触发更新

    • 重新渲染:重新执行Counter函数。
    • 重新构建Hooks链表:按顺序重新调用useState(返回更新后的状态)和useEffect
    • 重新收集副作用:因为count已变化,所以收集一个新的副作用(依赖项为1),并安排执行(同时清理上一次的副作用)。

总结

最后看下流程图:

这个函数是React执行函数组件的核心函数,它设置了Hooks的运行环境(通过设置当前dispatcher),执行组件函数,并在执行前后进行fiber状态的清理和设置。

这样设计使得Hooks能够正确地与当前渲染的fiber关联,并且区分挂载和更新两种状态。

相关推荐
LLLLYYYRRRRRTT10 分钟前
MariaDB 数据库管理与web服务器
前端·数据库·mariadb
胡gh11 分钟前
什么是瀑布流?用大白话给你讲明白!
前端·javascript·面试
universe_0117 分钟前
day22|学习前端ts语言
前端·笔记
teeeeeeemo20 分钟前
一些js数组去重的实现算法
开发语言·前端·javascript·笔记·算法
Zz_waiting.22 分钟前
Javaweb - 14.1 - 前端工程化
前端·es6
掘金安东尼24 分钟前
前端周刊第426期(2025年8月4日–8月10日)
前端·javascript·面试
Abadbeginning24 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
ZXT26 分钟前
WebAssembly
前端
卢叁26 分钟前
Flutter开发环境安装指南
前端·flutter
curdcv_po44 分钟前
Three.js,闲谈3D——智慧XX
前端