【React】调用 Hook 函数时发生了什么❓

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖

一、【使用示例】🚩

  • 文中都以 useState 这个常用的 Hook 函数为例进行说明,其他 Hook 函数的主要流程都是一样的,只是功能处理模块的内容不同

  • 使用的 React 为最新版本,源码可到 Github 获取

  • 示例代码(点击展开)

    javascript 复制代码
    import React, { useState } from 'react'
    
    const App = () => {
      const [num, setNum] = useState(0)
      const [age, setAge] = useState(3)
    
      const handleClick = () => {
        setNum(prevState => prevState + 1)  // num 会更新为 1
        setAge(prevState => prevState + 1)  // age 会更新为 4
      }
    
      return (
        <div>
          <p>{num} / {age}</p>
          <button onClick={handleClick}>+1</button>
        </div>
      )
    }
    
    export default App

二、【预备知识】

1. ⌈双缓存 Fiber 树⌋🍥

详细知识可查看专业人士的讲解👈

  • 在 React 中最多会存在两个 Fiber 树

  • 当前屏幕显示内容对应的是 current Fiber 树,树中的 Fiber 节点称为 current fiber

  • 正在内存中构建的是 workInProgress Fiber 树,树中的 Fiber 节点称为 workInProgress fiber

  • 两种 Fiber 节点之间通过 alternate 属性进行连接(点击展开)

    ini 复制代码
    currentFiber.alterbane = workInProgressFiber
    workInProgressFiber.alternate - currentFiber
  • React 应用的根节点通过使 current 指针在不同的 Fiber 树的 rootFiber 间切换来完成 current Fiber 树指向的切换
  • 当 workInProgress Fiber 树构建完成交给 Renderder 渲染在页面上后,应用根节点的 current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树
  • 每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新

2. ⌈hook 对象⌋🍥

  • memoizedState => 更新之后的最新状态值

  • baseState => 初始状态值,新的状态值会基于该值进行计算,即使用示例中的 prevState

  • baseQueue => 最新的更新任务队列

  • queue => 下面会进行说明

  • next => 指向下一个 hook 对象,形成 hook 单向链表

  • 代码截图(点击展开)

3. ⌈update 对象⌋🍥

这里只关注和 hook 相关的两个属性

  • action => 更新任务的具体更新操作,例如:使用示例中的 prevState => prevState + 1

  • next => 指向下一个 update 对象,形成一个单向链表

  • 代码截图(点击展开)

4. ⌈queue 对象⌋🍥

  • pending => 是由 update 对象组成的循环单向链表的头指针

  • lanes => 优先级,在任务调度时会使用到

  • dispatch => 用于更新状态的函数,例如:使用示例中的 setNum 函数

  • lastRenderedReducer => 上一次更新状态时的 reducer

  • lastRenderedState => 上一次的状态值(是用 lastRenderedReducer 计算出来的)

  • 代码截图(点击展开)

三、【查看引入的 useState 做了什么】🚩

源码中是在 packages/react/src/ReactHooks.js 中对 useState 做的功能封装和导出

  • 调用了 resolveDispatcher 函数,生成一个 dispatcher 对象

  • 然后返回 dispatcher.useState(initialState)

  • 这里的 initialState 就是我们传入的初始状态值,例如:使用示例中 const [num, setNum] = useState(0) 传入的就是 0

  • 代码截图(点击展开)

四、【resolveDispatcher 如何创建 dispatcher 】🚩

  • 获取 ReactCurrentDispatcher.current,并将其赋值给了创建的 dispatcher

  • 然后直接返回这个 dispatcher

  • 代码截图(点击展开)

五、【ReactCurrentDispatcher 是什么】🚩

ReactCurrentDispatcher 是从 packages/react/src/ReactCurrentDispatcher.js 中导入而来

  • 代码截图(点击展开)

  • ReactCurrentDispatcher.current 初始值是 null
  • 那就是说 resolveDispatcher 函数中返回的 dispatcher 就是 null
  • 那 useState 函数中返回的 dispatcher.useState(initialState) 岂不是会因为调用了 null.useState() 而导致报错<Cannot read properties of null (reading 'useState')>?

💡先别急,以上内容只是 useState 的功能封装,我们还没正式调用它。我们使用 useState 是在函数组件中,那么当项目运行,加载并渲染函数组件时,又会发生什么呢?

六、【运行项目之后】🚩

项目运行之后,整个过程会从 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork 函数开始

  • 对于函数组件,会调用 updateFunctionComponent 函数

  • 代码截图(点击展开)

七、【updateFunctionComponent 对函数组件做了什么处理】🚩

  • 该函数中处理 Hook 逻辑的是 renderWithHooks 函数

  • 当然还有一些其他处理,本文只关注对 Hook 的处理

  • 代码截图(点击展开)

八、【renderWithHooks 处理 Hook 逻辑】🚩

renderWithHooks 函数是从 packages/react-reconciler/src/ReactFiberHooks.js 中导入而来

  • 看到了一个熟悉的东西:ReactCurrentDispatcher.current

  • 判断语句中的 current 表示的是当前页面渲染内容所对应的 Fiber Tree

  • 但是我们才刚运行项目,还没有东西渲染出来,那么 current 当前的值也就是 null

  • 所以会将 HooksDispatcherOnMount 赋值给 ReactCurrentDispatcher.current

  • ReactCurrentDispatcher.current 不再是 null 了,那么第五步中提到的 dispatcher.useState(initialState) 就不会变成 null.useState() 而导致报错了👍

  • 代码截图(点击展开)

九、【HooksDispatcherOnMount 中有哪些内容】🚩

  • 给所有的 Hook 都指定了对应的处理函数

  • 我们这里只关注处理 useState 的 mountState 函数

  • 代码截图(点击展开)

十、【mountState 做了什么处理】🚩

  • 代码截图(点击展开)

  • 好像又有些眼熟的东西飘过了:[hook.memoizedState, dispatch]
  • 这里返回 [hook.memoizedState, dispatch],那么回到最开始的使用示例中
  • const [num, setNum] = useState(0) 就变成了 const [num, setNum] = [hook.memoizedState, dispatch]
  • 也就是说 hook.memoizedState 就是初始状态,而 dispatch 就是更改状态的操作函数
  • hook 是通过调用 mountStateImpl(initialState) 函数创建的
  • dispatch 是通过 dispatchSetState.bind() 创建的

💡.bind 不会执行函数,而是返回一个新的函数,这个函数中 this 对象的指向会被修改成我们指定的对象。也就是说 dispatchSetState.bind() 并没有执行 dispatchSetState 函数,而是返回了一个新的函数,当我们进行状态更改时才会执行这个函数,也就是使用示例中的 setNum(prevState => prevState + 1)

十一、【mountStateImpl(initialState) 创建 hook 对象】🚩

  • 代码截图(点击展开)

  • 调用 mountWorkInProgressHook 函数创建 hook 对象(点击展开)

    • 创建 hook 对象,该对象包含五个属性,各自记录了一些信息
    • 然后将创建的 hook 对象添加到 hook 链表的末尾
  • 初始化状态(点击展开)
    • 如果传入的 initialState 是一个函数,则将其执行获取到最终结果,再进行赋值操作
    • 如果不是函数,则直接赋值给 hook.memoizedState 和 hook.baseState
    • 现在再看 mountState 函数中返回的 [hook.memoizedState, dispatch],hook.memoizedState 就是我们使用 useState 时传入的初始值了
  • 创建更新任务队列,并将其赋值给 hook.queue
  • 最终返回 hook 对象

十二、【更新状态】🚩

通过上面的所有流程,我们得到了初始状态值和修改状态的函数。当我们执行这个函数修改状态时,又是什么样的流程呢?那就让我们看看这个修改状态的函数 dispatchSetState 中做了些什么事情。

  • 代码截图(点击展开)

  • 该函数接收三个参数(点击展开)
    • fiber => 当前正在处理的 Fiber 节点
    • queue => 更新任务队列
    • action => 新的状态值或函数
    • 前两个参数在 mountState 中通过 .bind() 调用当前函数时就传入了
    • 以 setNum(prevState => prevState + 1) 为例,prevState => prevState + 1 就是传入的第三个参数 action

1. ⌈创建更新任务⌋🍥

重点关注三个属性

  • lane => 表示本次更新任务的优先级

  • action => 本次更新任务的具体操作,使用示例中就是 prevState => prevState + 1

  • next => 指向下一个更新任务,当前创建的更新任务就是最新的,所以它的 next 属性值为 null

  • 代码截图(点击展开)

2. ⌈判断是否在渲染⌋🍥

  • 代码截图(点击展开)

  • 通过 isRenderPhaseUpdate 函数判断当前是不是正在渲染(点击展开)

  • 是正在渲染的话,会调用 enqueueRenderPhaseUpdate 函数,将创建的更新任务插入到循环更新任务队列
  • 不是的话,会获取最新的状态值,执行更新

3. ⌈插入循环更新任务队列⌋🍥

  • 先获取到当前缓存的更新任务队列(即 queue.pending )

  • 然后将上面创建的更新任务(即 update )添加到缓存更新任务队列的末尾

  • 代码截图(点击展开)

4. ⌈获取最新状态值⌋🍥

  • 代码截图(点击展开)

  • 获取 fiber.alternate(点击展开)
    • 此处的 fiber 是在 mount 阶段 dispatchSetState.bind() 时传入的,代表当前正在渲染的内容所对应的 Fiber Tree,也被称为 current Fiber Tree
    • 而它的 alternate 属性指向的是当前正在处理中的 Fiber Tree,即 workInProgress Fiber Tree
    • 但是此时才刚开始执行更新状态的函数,更新操作还未正式开始,所以此时的 fiber.alternate 为 null
    • 会进入 if 判断的匹配成功逻辑,进行操作
  • 通过 queue.lastRenderedReducer 获取到状态更新函数(点击展开)

    • 这里的 queue 是来自于 mount 阶段(通过 mountStateImpl 函数)生成的 hook 对象
    • 从第二张截图中可以看到,在创建 queue 对象时,lastRenderedReducer 属性被赋值为 basicStateReducer

  • 通过 queue.lastRenderedState 获取到 mount 阶段传入的初始状态值(点击展开)

  • 执行 lastRenderedReducer(currentState, action) 获取准备更新的状态值(点击展开)

    • 这里的 action 是我们在执行状态更新函数(例如:使用示例中的 setNum(prevState => prevState + 1) )时传入的参数
    • lastRenderedReducer 会先判断传入的 action 是否是个函数
    • prevState => prevState + 1 是一个函数,所以会将其执行,并将 currentState 作为参数传入
    • currentState 为 mount 阶段传入的初始值 0, 所以这里执行的结果会是 currentState = 1
    • 结果中的最新状态值 1 就是本次执行状态更新函数想要更新的结果
  • 通过 is(eagerState, currentState) 判断准备更新的状态值和当前状态值是否一致(点击展开)

    • 如果一致,就不会继续执行后续逻辑了
    • 所以当我们执行更新状态操作,但是想要更新的值和当前状态值一样时,函数组件是不会进行更新渲染的
  • 最后通过 scheduleUpdateOnFiber 进行任务调度(点击展开)

  • 任务调度完成之后,Reconciler 会开始工作,找出变化的组件,然后通知 Renderer 进行更新渲染

十三、【进行更新渲染】🚩

让我们回到第八步中的 renderWithHooks 函数中,当更新渲染时,因为页面已经有内容呈现出来了,我们是在做更新,所以 current 此时不再是 null 了。也就是说 ReactCurrentDispatcher.current 此时会被赋值为 HooksDispatcherOnUpdate,那我们就看看这个对象中有什么内容。

  • 与 HooksDispatcherOnMount 一样,HooksDispatcherOnUpdate 中也为各个 Hook 都指定了对应的处理函数

  • 我们这里还是只关注 useState,为它指定的处理函数是 updateState

  • 代码截图(点击展开)

十四、【updateState 做了什么处理】🚩

  • 该函数中其实是调用了 updateReducer 函数

  • 细心的朋友可能在上一步中会留意到,useReducer 这个 Hook 指定的处理函数也是 updateReducer

  • 其实 useReducer 是 useState 的升级版本,具体内容本文就不展开说明了,感兴趣的话,大家可以去 React 官网查看

  • 代码截图(点击展开)

十五、【updateReducer 的内部逻辑】🚩

  • 调用 updateWorkInProgressHook 函数获取 hook 对象

  • 调用 updateReducerImpl 函数做具体的更新处理

  • 代码截图(点击展开)

十六、【updateWorkInProgressHook 如何获取 hook 对象】🚩

  • 字段说明(点击展开)

    • currentlyRenderingFiber => 当前正在处理的 Fiber 节点
    • currentHook => 当前渲染内容所对应 Fiber 节点上的的 hook 链表
    • workInProgressHook => 当前生成(更新)的 hook 对象
  • 前置知识(点击展开)

    • 本文头部的示例代码在经过 mount 阶段进入到 update 阶段时,会形成一个如下图所示的数据结构
    • current 就是当前页面渲染的 App 组件对应的 Fiber 节点
    • currentlyRenderingFiber 是当前正在处理的 Fiber 节点
    • hook1 是 const [num, setNum] = useState(0) 产生的 hook 对象
    • hook2 是 const [age, setAge] = useState(3) 产生的 hook 对象
  • 代码截图(点击展开)

1. ⌈处理 nextCurrentHook⌋🍥

  • 代码截图(点击展开)

  • 在上面的字段说明中,可以看到 currentHook 初始值为 null,所以第一次处理时(即使用示例中的 setNum(...)),这里会进入到 if 判断的匹配成功逻辑

  • 当前是渲染更新,所以获取的 current 肯定是有值的

  • nextCurrentHook 会被赋值为 current.memoizedState

  • 结合上面的前置知识,可知 current.memoizedState 是一个单向的 hook 链表

  • 根据使用示例来说明的话,这里的 hook 链表中存储了两个 hook 对象,分别记录了对应的信息

  • 当第二次处理时(即使用示例中的 setAge(...)),currentHook 在上次处理完已经有值了,nextCurrentHook 会直接获取 currentHook.next 的值

  • 我们在控制台输出一下处理后的 nextCurrentHook(点击展开)

    • 第一条是在处理 const [num, setNum] = useState(0) 时的输出信息,它的 next 属性指向的是下一个 hook 对象,也就是输出的第二条信息
    • 第二条是在处理 const [age, setAge] = useState(3) 时的输出信息,它后面没有 hook 对象了,所以它的 next 属性为 null
    • 可以结合上面的前景知识来进行理解

2. ⌈处理 nextWorkInProgressHook⌋🍥

  • 代码截图(点击展开)

  • 在上面的字段说明中,可以看到 workInProgressHook 初始值为 null,所以这里会进入到 if 判断的匹配成功逻辑

  • nextWorkInProgressHook 会被赋值为 currentlyRenderingFiber.memoizedState

  • 我们在控制台输出一下处理后的 nextWorkInProgressHook(点击展开)

    • currentlyRenderingFiber 是正在处理中的 Fiber 节点,目前还没有设置其 memoizedState 属性
    • 所以 currentlyRenderingFiber.memoizedState 获取到的,是在创建 Fiber 节点时设置的初始值,即 null

3. ⌈处理 workInProgressHook⌋🍥

  • 代码截图(点击展开)

  • 上一步中提到 nextWorkInProgressHook 被赋值为 currentlyRenderingFiber.memoizedState,而其值为 null,所以这里会进入 if 判断的匹配失败(即 else )逻辑中

  • 上面提到 nextCurrentHook 在处理后是有值的,所以不会执行 nextCurrentHook === null 的判断成功逻辑

  • 然后 currentHook 会被赋值为 nextCurrentHook

  • 接着创建一个新的 hook 对象,其属性信息是从 currentHook 中获取的(点击展开)

    • 由于 currentHook = nextCurrentHook = current.memoizedState,所以 currentHook 其实就是 mount 阶段生成的 hook
    • 所以创建的这个新 hook 对象,其实是对 mount 阶段生成的 hook 的一个浅克隆
    • 需要注意的是,这里创建的 hook 对象,其 next 属性的值都是 null
  • 在上面的字段说明中,可以看到 workInProgressHook 初始值为 null

  • 所以第一次处理时(即使用示例中的 setNum(...)),会将创建的新 hook 对象赋值给 currentlyRenderingFiber.memoizedState 和 workInProgressHook

  • 当我们第二次处理的时候(即使用示例中的 setAge(...)),workInProgressHook 就不是 null 了,所以会将新创建的 hook 对象添加到当前 hook 链表的末尾

  • 然后返回 workInProgressHook 到 updateReducer 函数中

  • 所以说,我们在 update 阶段获取到的 hook 对象,和 mount 阶段生成的 hook 对象不是同一个,而是对其做了浅克隆

  • 我们在控制台输出一下处理后的 workInProgressHook(点击展开)

十七、【updateReducerImpl 拿着 hook 对象做了什么】🚩

  • 代码截图(点击展开)

1. ⌈处理 pendingQueue⌋🍥

  • 代码截图(点击展开)

  • 看看 hook.queue 包含什么信息(点击展开)

  • 将传入的 reducer (即使用示例中的 prevState => prevState + 1) 赋值给 queue.lastRenderedReducer

  • 看看 pendingQueue 包含什么信息(点击展开)

    • 因为是循环任务队列,所以队列中的最后一个更新任务的 next 属性会指向队列中的第一项
    • 而目前就只有一条更新任务,所以它的 next 属性会指向它自己
  • 再看看 baseQueue 包含什么信息(点击展开)

    • 目前还没有更新任务存在,所以值为 null
  • 将待更新任务队列(pendingQueue)合并到循环更新任务队列(baseQueue)中(点击展开)

    • 下图为合并后的循环任务队列信息
    • 可以看到上面的 pendingQueue 中的更新任务被加入进来了

2. ⌈处理 baseQueue⌋🍥

  • 循环获取更新任务,执行具体更新操作

  • 将更新后的状态值赋值给 hook.memoizedState 和 hook.baseState

  • 将处理之后的循环任务队列赋值给 hook.baseQueue

  • 代码截图(点击展开)

3. ⌈返回处理结果⌋🍥

  • 代码截图(点击展开)

  • 创建 dispatch 并赋值为 queue.dispatch(点击展开)

    • 上面提到过,update 阶段获取到的 hook 对象是对 mount 阶段生成的 hook 对象的浅克隆
    • 所以 hook.queue.dispatch 就是 mount 阶段通过 dispatchSetState.bind() 返回的函数
  • 返回 [hook.memoizedState, dispatch],这里的 hook.memoizedState 在上一步处理 baseQueue 时,已经被赋值为更新后的状态了
  • 所以当重新渲染函数组件时,会再执行一次 const [num, setNum] = useState(1)
  • 但是此次执行函数组件,当前的作用域和上次不一样了(因为函数组件本质就是一个函数),当前作用域中的状态值不再是初始值0,而是更新后的状态值 1
  • 至此,更新就完成了。页面上显示的 num 为 1,显示的 age 为 4

【流程总结】🚩

【说明】🚩

  • 文中的代码截图来自于克隆到本地的官方源码

  • 本文内容是作者自己的理解和书写思路,若有错误的地方,欢迎指正

  • 有不清楚的内容,欢迎在评论区中讨论,也可加入作者的学习群一起交流学习(点击展开)

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢
相关推荐
夏花里的尘埃17 分钟前
vue3实现echarts——小demo
前端·vue.js·echarts
努力学习的木子1 小时前
uniapp如何隐藏默认的页面头部导航栏,uniapp开发小程序如何隐藏默认的页面头部导航栏
前端·小程序·uni-app
java小郭4 小时前
html的浮动作用详解
前端·html
水星记_4 小时前
echarts-wordcloud:打造个性化词云库
前端·vue
强迫老板HelloWord5 小时前
前端JS特效第22波:jQuery滑动手风琴内容切换特效
前端·javascript·jquery
续亮~6 小时前
9、程序化创意
前端·javascript·人工智能
RainbowFish6 小时前
「Vue学习之路」—— vue的常用指令
前端·vue.js
Wang's Blog6 小时前
Webpack: 三种Chunk产物的打包逻辑
前端·webpack·node.js
pan_junbiao7 小时前
HTML5使用<blockquote>标签:段落缩进
前端·html·html5
38kcok9w2vHanx_7 小时前
从0开始搭建vue项目
前端·javascript·vue.js