react的useState源码分析
前言
简单说下为什么React选择函数式组件,主要是class组件比较冗余、生命周期函数写法不友好,骚写法多,functional组件更符合React编程思想等等等。更具体的可以拜读dan大神的blog。其中Function components capture the rendered values这句十分精辟的道出函数式组件的优势。
但是在16.8之前react的函数式组件十分羸弱,基本只能作用于纯展示组件,主要因为缺少state和生命周期。本人曾经在hooks出来前负责过纯函数式的react项目,所有状态处理都必须在reducer中进行,所有副作用都在saga中执行,可以说是十分艰辛的经历了。在hooks出来后我在公司的一个小中台项目中使用,落地效果不错,代码量显著减少的同时提升了代码的可读性。因为通过custom hooks可以更好地剥离代码结构,不会像以前类组件那样在cDU等生命周期堆了一大堆逻辑,在命令式代码和声明式代码中有一个良性的边界。
useState在React中是怎么实现的
Hooks take some getting used to — and especially at the boundary of imperative and declarative code.
如果对hooks不太了解的可以先看看这篇文章:前情提要,十分简明的介绍了hooks的核心原理,但是我对useEffect,useRef等钩子的实现比较好奇,所以开始啃起了源码,下面我会结合源码介绍useState的原理。useState具体逻辑分成三部分:mountState,dispatch, updateState
hook的结构
首先的是hooks的结构,hooks是挂载在组件Fiber结点上memoizedState的
//hook的结构
export type Hook = {memoizedState: any, //上一次的statebaseState: any, //当前statebaseUpdate: Update<any, any> | null, // update funcqueue: UpdateQueue<any, any> | null, //用于缓存多次actionnext: Hook | null, //链表
};
renderWithHooks
在reconciler中处理函数式组件的函数是renderWithHooks,其类型是:
renderWithHooks(current: Fiber | null, //当前的fiber结点workInProgress: Fiber, Component: any, //jsx中用<>调用的函数props: any,refOrContext: any,nextRenderExpirationTime: ExpirationTime, //需要在什么时候结束
): any
在renderWithHooks,核心流程如下:
//从memoizedState中取出hooks
nextCurrentHook = current !== null ? current.memoizedState : null;
//判断通过有没有hooks判断是mount还是update,两者的函数不同
ReactCurrentDispatcher.current =nextCurrentHook === null? HooksDispatcherOnMount: HooksDispatcherOnUpdate;
//执行传入的type函数
let children = Component(props, refOrContext);
//执行完函数后的dispatcher变成只能调用context的
ReactCurrentDispatcher.current = ContextOnlyDispatcher;return children;
useState构建时流程
mountState
在HooksDispatcherOnMount中,useState调用的是下面的mountState,作用是创建一个新的hook并使用默认值初始化并绑定其触发器,因为useState底层是useReducer,所以数组第二个值返回的是dispatch。
type BasicStateAction<S> = (S => S) | S;function mountState<S>(initialState: (() => S) | S,
){const hook = mountWorkInProgressHook();
//如果入参是func则会调用,但是不提供参数,带参数的需要包一层if (typeof initialState === 'function') {initialState = initialState();}
//上一个state和基本(当前)state都初始化hook.memoizedState = hook.baseState = initialState;const queue = (hook.queue = {last: null,dispatch: null,eagerReducer: basicStateReducer, // useState使用基础reducereagerState: (initialState: any),});
//返回触发器const dispatch: Dispatch<//useState底层是useReducer,所以type是BasicStateAction(queue.dispatch = (dispatchAction.bind(null,//绑定当前fiber结点和queue((currentlyRenderingFiber: any): Fiber),queue,): any));return [hook.memoizedState, dispatch];
}
mountWorkInProgressHook
这个函数是mountState时调用的构建hook的方法,在初始化完毕后会连接到当前hook.next(如果有的话)
function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,queue: null,baseUpdate: null,next: null,};if (workInProgressHook === null) {// 列表中的第一个hookfirstWorkInProgressHook = workInProgressHook = hook;} else {// 添加到列表的末尾workInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}
dispatch分发函数
在上面我们提到,useState底层是useReducer,所以返回的第二个参数是dispatch函数,其中的设计十分巧妙。
假设我们有以下代码:
相关参考视频讲解:进入学习
const [data, setData] = React.useState(0)
setData('first')
setData('second')
setData('third')

在第一次setData后, hooks的结构如上图

在第二次setData后, hooks的结构如上图

在第三次setData后, hooks的结构如上图

在正常情况下,是不会在dispatcher中触发reducer而是将action存入update中在updateState中再执行,但是如果在react没有重渲染需求的前提下是会提前计算state即eagerState。作为性能优化的一环。
function dispatchAction<S, A>(fiber: Fiber,queue: UpdateQueue<S, A>,action: A,
) {const alternate = fiber.alternate;{flushPassiveEffects();//获取当前时间并计算可用时间const currentTime = requestCurrentTime();const expirationTime = computeExpirationForFiber(currentTime, fiber);const update: Update<S, A> = {expirationTime,action,eagerReducer: null,eagerState: null,next: null,};//下面的代码就是为了构建queue.last是最新的更新,然后last.next开始是每一次的action// 取出lastconst last = queue.last;if (last === null) {// 自圆update.next = update;} else {const first = last.next;if (first !== null) {update.next = first;}last.next = update;}queue.last = update;if (fiber.expirationTime === NoWork &&(alternate === null || alternate.expirationTime === NoWork)) {// 当前队列为空,我们可以在进入render阶段前提前计算出下一个状态。如果新的状态和当前状态相同,则可以退出重渲染const lastRenderedReducer = queue.lastRenderedReducer; // 上次更新完后的reducerif (lastRenderedReducer !== null) {let prevDispatcher;if (__DEV__) {prevDispatcher = ReactCurrentDispatcher.current; // 暂存dispatcherReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;}try {const currentState: S = (queue.lastRenderedState: any);// 计算下次stateconst eagerState = lastRenderedReducer(currentState, action);// 在update对象中存储预计算的完整状态和reducer,如果在进入render阶段前reducer没有变化那么可以服用eagerState而不用重新再次调用reducerupdate.eagerReducer = lastRenderedReducer;update.eagerState = eagerState;if (is(eagerState, currentState)) {// 在后续的时间中,如果这个组件因别的原因被重渲染且在那时reducer更变后,仍有可能重建这次更新return;}} catch (error) {// Suppress the error. It will throw again in the render phase.} finally {if (__DEV__) {ReactCurrentDispatcher.current = prevDispatcher;}}}}scheduleWork(fiber, expirationTime);}
}
useState更新时流程
updateReducer
因为useState底层是useReducer,所以在更新时的流程(即重渲染组件后)是调用updateReducer的。
function updateState<S>(initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {return updateReducer(basicStateReducer, (initialState: any));
}
所以其reducer十分简单
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {return typeof action === 'function' ? action(state) : action;
}
我们先把复杂情况抛开,跑通updateReducer流程
function updateReducer(reducer: (S, A) => S,initialArg: I,init?: I => S,
){// 获取当前hook,queueconst hook = updateWorkInProgressHook();const queue = hook.queue;queue.lastRenderedReducer = reducer;// action队列的最后一个更新const last = queue.last;// 最后一个更新是基本状态const baseUpdate = hook.baseUpdate;const baseState = hook.baseState;// 找到第一个没处理的更新let first;if (baseUpdate !== null) {if (last !== null) {// 第一次更新时,队列是一个自圆queue.last.next = queue.first。当第一次update提交后,baseUpdate不再为空即可跳出队列last.next = null;}first = baseUpdate.next;} else {first = last !== null ? last.next : null;}if (first !== null) {let newState = baseState;let newBaseState = null;let newBaseUpdate = null;let prevUpdate = baseUpdate;let update = first;let didSkip = false;do {const updateExpirationTime = update.expirationTime;if (updateExpirationTime < renderExpirationTime) {// 优先级不足,跳过这次更新,如果这是第一次跳过更新,上一个update/state是newBaseupdate/stateif (!didSkip) {didSkip = true;newBaseUpdate = prevUpdate;newBaseState = newState;}// 更新优先级if (updateExpirationTime > remainingExpirationTime) {remainingExpirationTime = updateExpirationTime;}} else {// 处理更新if (update.eagerReducer === reducer) {// 如果更新被提前处理了且reducer跟当前reducer匹配,可以复用eagerStatenewState = ((update.eagerState: any): S);} else {// 循环调用reducerconst action = update.action;newState = reducer(newState, action);}}prevUpdate = update;update = update.next;} while (update !== null && update !== first);if (!didSkip) {newBaseUpdate = prevUpdate;newBaseState = newState;}// 只有在前后state变了才会标记if (!is(newState, hook.memoizedState)) {markWorkInProgressReceivedUpdate();}hook.memoizedState = newState;hook.baseUpdate = newBaseUpdate;hook.baseState = newBaseState;queue.lastRenderedState = newState;}const dispatch: Dispatch<A> = (queue.dispatch: any);return [hook.memoizedState, dispatch];
}
export function markWorkInProgressReceivedUpdate() {didReceiveUpdate = true;
}
后记
作为系列的第一篇文章,我选择了最常用的hooks开始,抛开提前计算及与react-reconciler的互动,整个流程是十分清晰易懂的。mount的时候构建钩子,触发dispatch时按序插入update。updateState的时候再按序触发reducer。可以说就是一个简单的redux。
相关文章:
react的useState源码分析
前言 简单说下为什么React选择函数式组件,主要是class组件比较冗余、生命周期函数写法不友好,骚写法多,functional组件更符合React编程思想等等等。更具体的可以拜读dan大神的blog。其中Function components capture the rendered values这句…...
SharpImpersonation:一款基于令牌和Shellcode注入的用户模拟工具
关于SharpImpersonation SharpImpersonation是一款功能强大的用户模拟工具,该工具基于令牌机制和Shellcode注入技术实现其功能,可以帮助广大研究人员更好地对组织内部的网络环境和系统安全进行分析和测试。 该工具基于 Tokenvator的代码库实现其功能&a…...
华为OD机试 - 最大相连男生数(Python)| 真题+思路+代码
最大相连男生数 题目 学校组织活动,将学生排成一个矩形方阵。 请在矩形方阵中找到最大的位置相连的男生数量。 这个相连位置在一个直线上,方向可以是水平的、垂直的、成对角线的或者反对角线的。 注:学生个数不会超过 10000。 输入 输入的第一行为矩阵的行数和列数,接下…...
GIS在地质灾害危险性评估与灾后重建中的实践技术应用及python机器学习灾害易发性评价模型建立与优化
地质灾害是指全球地壳自然地质演化过程中,由于地球内动力、外动力或者人为地质动力作用下导致的自然地质和人类的自然灾害突发事件。由于降水、地震等自然作用下,地质灾害在世界范围内频繁发生。我国除滑坡灾害外,还包括崩塌、泥石流、地面沉…...
2.13、进程互斥的硬件实现方法
1、中断屏蔽方法 利用 “开/关中断指令” 实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况) 优点:简单…...
Leetcode力扣秋招刷题路-2335
从0开始的秋招刷题路,记录下所刷每道题的题解,帮助自己回顾总结 2335. 装满杯子需要的最短总时长 现有一台饮水机,可以制备冷水、温水和热水。每秒钟,可以装满 2 杯 不同 类型的水或者 1 杯任意类型的水。 给你一个下标从 0 开…...
C语言深度解剖-关键字(6)
目录 1. 浮点型与零的比较: 1.1 推导: 1.2 实践: 总结: 理解强制类型转换: 指针与零比较 switch case 语句: 写在最后: 1. 浮点型与零的比较: 1.1 推导: 例&am…...
[多线程进阶]CAS与Synchronized基本原理
专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1.CAS 1.1 什么是CAS? 1.2 CAS伪代码 1.3 CAS …...
【Linux系统编程】02:文件操作
文件IO 系统调用(不带缓冲的IO操作)库函数(默认带用户缓冲的IO操作) 一、非缓冲IO 系统调用:即为不带缓冲的IO 1.打开文件open 2.读取文件read NAMEread - read from a file descriptorSYNOPSIS#include <unist…...
华为OD机试 - 去除多余空格(Python)| 真题+思路+代码
去除多余空格 题目 去除文本多余空格,但不去除配对单引号之间的多余空格。给出关键词的起始和结束下标,去除多余空格后刷新关键词的起始和结束下标。 条件约束: 不考虑关键词起始和结束位置为空格的场景;单词的的开始和结束下标保证涵盖一个完整的单词,即一个坐标对开…...
百趣代谢组学分享,补充α-酮酸的低蛋白饮食对肾脏具有保护作用
文章标题:Reno-Protective Effect of Low Protein Diet Supplemented With α-Ketoacid Through Gut Microbiota and Fecal Metabolism in 5/6 Nephrectomized Mice 发表期刊:Frontiers in Nutrition 影响因子:6.59 作者单位:…...
json对象和formData相互转换
前言 大家都知道,前端在和后台进行交互联调时,肯定避免不了要传递参数,一般情况下,params 在 get 请求中使用,而 post 请求下,我们有两种常见的传参方式: JSON 对象格式和 formData 格式&#x…...
【c++面试问答】常量指针和指针常量的区别
问题 常量指针和指针常量有什么区别? const的优点 在C中,关键字const用来只读一个变量或对象,它有以下几个优点: 便于类型检查,如函数的函数 func(const int a) 中a的值不允许变,这样便于保护实参。功能…...
Ubuntu18下编译android的ffmpeg经验
虽然按照网上的一些资料(如:最简单的基于FFmpeg的移动端例子:Android HelloWorld_雷霄骅的博客-CSDN博客_android ffmpeg 例子,,编译FFmpeg4.1.3并移植到Android app中使用(最详细的FFmpeg-Android编译教程…...
Spring Security in Action 第十三章 实现OAuth2的认证端
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
本文章提供中国国界、国界十段线原始数据以及加载方法
本文章提供中国国界九段线原始数据和加载方法 1、中国国界 完整数据 包括十段线 中国国界线(完整版 包括十段线) 2、原始数据 中国国界十段线topojson格式数据.rar 中国国界线topjson数据 中国国界十段线svg格式数据.rar 中国国界线svg数据 中国国界十段线shp格式数据…...
一文带你搞懂,Python语言运算符
Python语言支持很多种运算符,我们先用一个表格为大家列出这些运算符,然后选择一些马上就会用到的运算符为大家进行讲解。 说明:上面这个表格实际上是按照运算符的优先级从上到下列出了各种运算符。所谓优先级就是在一个运算的表达式中&#x…...
JAVA集合专题4 —— Map
目录Map接口实现类的特点Map接口的常见方法Map六大遍历方式Map练习1code编程练习2code编程练习3思路codeMap接口实现类的特点 Map与Collection并列存在,是Map集合体系的顶级接口Map的有些子实现存储数据是有序的(LinkedHashMap),有些子实现存储数据是无…...
二叉树进阶--二叉搜索树
目录 1.二叉搜索树 1.1 二叉搜索树概念 1.2 二叉搜索树操作 1.3 二叉搜索树的实现 1.4 二叉搜索树的应用 1.5 二叉搜索树的性能分析 2.二叉树进阶经典题: 1.二叉搜索树 1.1 二叉搜索树概念 二叉搜索树又称二叉排序树,它或者是一棵空树,…...
牛客网Python篇数据分析习题(三)
1.现有一个Nowcoder.csv文件,它记录了牛客网的部分用户数据,包含如下字段(字段与字段之间以逗号间隔): Nowcoder_ID:用户ID Level:等级 Achievement_value:成就值 Num_of_exercise&a…...
如何用Rusted PackFile Manager彻底重构全面战争模组开发工作流?
如何用Rusted PackFile Manager彻底重构全面战争模组开发工作流? 【免费下载链接】rpfm Rusted PackFile Manager (RPFM) is a... reimplementation in Rust and Qt6 of PackFile Manager (PFM), one of the best modding tools for Total War Games. 项目地址: h…...
手把手教你用CH342 USB转串口模块在Ubuntu 22.04上调试(附dmesg日志分析)
手把手教你用CH342 USB转串口模块在Ubuntu 22.04上调试(附dmesg日志分析) 嵌入式开发中,串口调试是最基础却最容易出问题的环节。当你在Ubuntu 22.04上插入CH342模块准备调试ESP32开发板时,是否遇到过设备无法识别、权限拒绝或者波…...
【NotebookLM音频黑科技深度解析】:20年AI产品经理亲测的5大颠覆性功能与3个未公开技巧
更多请点击: https://intelliparadigm.com 第一章:NotebookLM Audio Overview NotebookLM Audio 是 Google 推出的实验性语音增强功能,深度集成于 NotebookLM 平台,旨在将用户上传的 PDF、网页文本等资料转化为可交互的语音知识体…...
告别重启!IDEA里用JRebel插件实现Java代码秒级热更新(附最新激活与配置避坑指南)
告别重启!IDEA里用JRebel插件实现Java代码秒级热更新(附最新激活与配置避坑指南) 作为一名长期与Java打交道的开发者,你是否经历过这样的痛苦循环:修改一行代码 → 保存 → 等待漫长的Tomcat重启 → 验证修改 → 发现…...
3分钟掌握B站缓存视频转换:m4s转MP4的完整免费解决方案
3分钟掌握B站缓存视频转换:m4s转MP4的完整免费解决方案 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾为B站下架的视频感到…...
3分钟掌握清华PPT模板:免费打造专业学术演示文稿的终极方案
3分钟掌握清华PPT模板:免费打造专业学术演示文稿的终极方案 【免费下载链接】THU-PPT-Theme 清华主题PPT模板 项目地址: https://gitcode.com/gh_mirrors/th/THU-PPT-Theme 还在为学术汇报、毕业答辩或重要演讲的PPT设计而头疼吗?清华大学视觉设计…...
Windows 11终极优化指南:如何用Win11Debloat快速清理系统垃圾与保护隐私
Windows 11终极优化指南:如何用Win11Debloat快速清理系统垃圾与保护隐私 【免费下载链接】Win11Debloat A simple, lightweight PowerShell script that allows you to remove pre-installed apps, disable telemetry, as well as perform various other changes to…...
HBase集群部署避坑指南:从NoNode for /hbase/master错误到稳定启动
1. 遇到NoNode错误时别慌,先看懂它在说什么 第一次看到"HBase报错ERROR: KeeperErrorCode NoNode for /hbase/master"这个错误时,我正端着咖啡准备庆祝集群启动成功。结果这行红字直接给我泼了盆冷水——相信很多新手朋友都有类似的经历。这个…...
Vibe Stack 全栈开发实战:30分钟构建SaaS应用的技术解析
1. 从零到一:我如何用 Vibe Stack 在 30 分钟内搭建一个可用的 SaaS 应用 作为一名在 Web 开发领域摸爬滚打了十多年的老程序员,我见过太多“五分钟快速启动”的噱头,最后往往需要花上五个小时去解决各种环境配置和依赖冲突。所以࿰…...
AI编程协作实战:从提示工程到全周期开发工作流
1. 项目概述:从零开始的AI编程启蒙最近在GitHub上看到一个挺有意思的项目,叫jnMetaCode/ai-coding-101。光看名字,你大概能猜到它和AI编程有关,但“101”这个后缀又暗示着它的入门属性。作为一个在代码堆里摸爬滚打多年的老码农&a…...
