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…...
Java开发常见关键词集绵
一、关键词1: (1)RPC:远程过程调用(Remote Procedure Call)的缩写形式。远程调用的时候让人们觉得是本地调用。 (2)HTTP:超文本传输协议(Hyper Text Transfer…...
解决idea出现的java.lang.OutOfMemoryError: Java heap space的问题
文章目录1. 复现问题2. 分析问题3. 解决问题4. 补充解决java.lang.OutOfMemoryError: PermGen space问题1. 复现问题 今天使用idea开发时,突然报出如下错误: Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat org.…...
为什么子进程要继承处理器亲缘性?
请先考虑一个典型的程序为什么需要启动一个子进程。(当然资源管理器不算一个典型的程序) 这是因为手头的任务被分解为子任务,无论出于何种原因,这些子任务都被放入子流程中。例如,在实现多次遍历型编译器/链接器时,其中每次遍历都…...
【算法】高精度
作者:指针不指南吗 专栏:算法篇 🐾不能只会思路,必须落实到代码上🐾 文章目录前言一、高精度加法二、高精度减法三、高精度乘法四、高精度除法前言 高精度即很大很大的数,超过了 long long 的范围&…...
计算机网络-基本概念
目录 计算机网络-基本概念 互联网 Java的跨平台原理 编辑 C\C的跨平台原理 解释性语言的跨平台原理(python,js等) 客户端 vs 服务器 什么是协议? 网络互连模型 请求过程 计算机之间的通信基础 计算机之间的连接方式-网线直连(需要用交叉线,而…...
你评论,我赠书~【哈士奇赠书 - 13期】-〖Python程序设计-编程基础、Web开发及数据分析〗参与评论,即可有机获得
大家好,我是 哈士奇 ,一位工作了十年的"技术混子", 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 💬 人生格言:优于别人,并不高贵,真正的高贵应该是优于过去的自己。💬 ὎…...
【设计模式】我终于读懂了代理模式。。。
👦代理模式的基本介绍 1)代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。 2)被代理的对象可以是远程对象、创建…...
每天10个前端小知识 【Day 2】
👩 个人主页:不爱吃糖的程序媛 🙋♂️ 作者简介:前端领域新星创作者、CSDN内容合伙人,专注于前端各领域技术,成长的路上共同学习共同进步,一起加油呀! ✨系列专栏:前端…...
帮助中心在线制作工具推荐这4款,很不错哟!
根据用户咨询问题是否解决的情景,分为三个部分,首先帮助中心恰好有用户需要咨询的问题,用户可以通过点击相关问题即可解决自己的问题,其次,用户第一眼没有在帮助中心解决问题,有个搜索框,用户的…...
rabbitMQ相关文章汇总
RabbitMQ五种工作模式: https://blog.csdn.net/weixin_41882200/article/details/117128590?ops_request_misc%257B%2522request%255Fid%2522%253A%2522167625223516800182771874%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id1…...
韩国网站加速器/如何让百度收录网址
安装Python包python-pptx需要用到lxml,而安装lxml报错: fatal error: libxml/xmlversion.h file not found 解决方法: xcode-select --install 安装完commandline tool for xcode后,在安装就不会出错了。 本文转自 h2appy 51CTO博…...
不花钱建网站/站长工具seo综合查询 分析
美国当地时间 8 月 19 日,旨在促进 Linux 普及的非营利团体 Open SourceDevelopmentLab(OSDL,开放源码开发实验室)发布了用来自动测试 Linux 内核的“ScalableTestPlatform(STP)”环境的最新版本 Version3.…...
房地产网站建设提案/权威解读当前经济热点问题
和组合电路不同,当输入改变时,输出并不会马上改变。而是需要时钟信号改变时,输出会更新。 需要时序电路的原因很简单:当我们需要一个循环时,我们希望控制循环的变量是离散的。 如果i1时进行循环,如果i1的…...
b2b网站建设方案长沙/餐饮营销方案
欢迎关注”生信修炼手册”!和GEO数据库类似,ArrayExpress是属于EBI旗下的公共数据库,用于存放芯片和高通量测序的相关数据,网址如下>https://www.ebi.ac.uk/arrayexpress/数据来源于下图所示的两个部分第一部分是由科研工作者提交的数据&a…...
域名有wordpress/应用商店下载安装
不同SD记忆卡厂商及品牌的记忆卡之实际传输速度有時會有很大差异。对影片录像来说需要非常高稳定的最小写入速度,因此各式各样不同写入速度的记忆卡,让人难以决定该选择哪一张卡,才能确保播放顺畅。因此,SD协会将速度等级规格&…...
德育工作网站建设方案/百度推广代理开户
试验网站#3搜索引擎优化收录情况记录日期Yahoogooglebaidusogou每日收录每日收录增量每日收录每日收录增量每日收录每日收录增量每日收录每日收录增量2007-7-7迁移至不限带宽的服务器,地理位置在美国2007-7-193650 1 6 0 2007-7-253703531141132216002007-7-263705…...