⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖
一、【使用示例】🚩
-
文中都以 useState 这个常用的 Hook 函数为例进行说明,其他 Hook 函数的主要流程都是一样的,只是功能处理模块的内容不同
-
使用的 React 为最新版本,源码可到 Github 获取
-
示例代码(点击展开)
javascriptimport 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 属性进行连接(点击展开)
inicurrentFiber.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
【流程总结】🚩
【说明】🚩
-
文中的代码截图来自于克隆到本地的官方源码
-
本文内容是作者自己的理解和书写思路,若有错误的地方,欢迎指正
-
有不清楚的内容,欢迎在评论区中讨论,也可加入作者的学习群一起交流学习(点击展开)
- 如果本文对您有帮助,烦请动动小手点个赞,谢谢