你真的了解 setState 吗?
本文所有示例
setState 算是 React 里被使用的最高频的 api,但你真的了解 setState 吗?比如下面这段代码,你能清楚的知道输出什么吗?
import { Component } from 'react'
export class stateDemo extends Component {state = {count: 0}componentDidMount() {this.setState({ count: this.state.count + 1 })console.log(this.state.count)this.setState({ count: this.state.count + 1 })console.log(this.state.count)setTimeout(() => {this.setState({ count: this.state.count + 1 })console.log(this.state.count)this.setState({ count: this.state.count + 1 })console.log(this.state.count)}, 0)}render() {return null}
}export default stateDemo
要彻底弄懂这道题,就不得不聊 setState
的异步更新,另外输出结果也要看当前处于哪种模式下。
我们先从 setState
的用法说起,以便全面掌握
1、为什么需要 setState
虽然我们一直在用 setState
,可有没想过为什么 React 里会有该 api
?
React 是通过管理状态来实现对组件的管理,即 UI = f(state)
f 就是我们的代码,最主要的就是 this.setState
,调用该函数后 React 会使用更新的 state
重新渲染此组件及其子组件,即达到了 UI 层的变更。
2、什么是 setState
setState
是 React 官方提供的更新 state
的方法,通过调用 setState
,React 会使用最新的 state 值,并调用 render
方法将变化展现到视图。
在 React v16.3 版本之前,调用 setState
方法会依次触发以下生命周期函数
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
那么 state 在哪个生命周期里会更新为最新的值?
import React, { Component } from 'react'
export default class stateDemo2 extends Component {state = {count: 0}shouldComponentUpdate() {console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0return true}componentWillUpdate() {console.info('componentWillUpdate', this.state.count) // componentWillUpdate 0}increase = () => {this.setState({count: this.state.count + 1})}render() {console.info('render', this.state.count) // render 1return (<div><p>{this.state.count}</p><button onClick={this.increase}>累加</button></div>)}componentDidUpdate() {console.info('componentDidUpdate', this.state.count) // componentDidUpdate 1}
}
可以看到,直到 render
执行时,state
的值才变更为最新的值,在此之前,state
一直保持为更新前的状态。
见示例库里的 stateDemo2.js
在 React v16.3 版本之后,调用 setState
方法会依次触发以下生命周期函数
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
确切的说,应该是 v16.4 版本之后,v16.3 版本 setState 并不会触发 getDerivedStateFromProps 函数
那么 state 在哪个生命周期里会更新为最新的值?
import React, { Component } from 'react'export default class stateDemo3 extends Component {state = {count: 0}static getDerivedStateFromProps(props, state) {console.info('getDerivedStateFromProps', state.count) // getDerivedStateFromProps 1return { ...state }}shouldComponentUpdate() {console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0return true}increase = () => {this.setState({count: this.state.count + 1})}render() {console.info('render', this.state.count) //render 1return (<div><p>{this.state.count}</p><button onClick={this.increase}>累加</button></div>)}getSnapshotBeforeUpdate() {console.info('getSnapshotBeforeUpdate', this.state.count) //getSnapshotBeforeUpdate 1return null}componentDidUpdate() {console.info('componentDidUpdate', this.state.count) //componentDidUpdate 1}
}
可以看到新增的两个生命周期函数 getDerivedStateFromProps
与 getSnapshotBeforeUpdate
获取到的 state
都是新值
见示例库里的 stateDemo3.js
3、setState 用法
3.1、setState(stateChange[, callback])
第一个参数是一个对象,会将传入的对象浅层合并到 ;第二个参数是个可选的回调函数
例如,调整购物车商品数:
this.setState({quantity: 2})
在回调函数参数里,可以获取到最新的 state
值,但推荐使用 componentDidUpdate
3.2、setState(updater, [callback])
第一个参数是个函数,(state, props) => stateChange,第二个参数同上是个可选的回调函数
例如:
this.setState((state, props) => {return {counter: state.counter + props.step};
});
updater 函数中接收的 state 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。
4、state 的不可变性
我们要严格遵行 state
是不可变的原则,即不可以直接修改 state
变量,例如底下的做法就是不可取的
this.state.count ++
this.state.count ++
this.setState({})
这样是实现了同步更改 state
的目的,但违背了 state
是不可变的原则
4.1、基本数据类型
this.setState({count: 1,name: 'zhangsan',flag: true
})
4.2、对象类型
使用 ES6 的 Object.assign
或解构赋值
this.setState({// person: Object.assign({}, this.state.person, { name: 'lisi' })person:{...this.state.person,age:22}
})
见示例库里的 stateDemo4.js
4.3、数组类型
- 追加选项: 使用
concat
或者解构赋值
this.setState((prevState) => {return {// hobbys: prevState.hobbys.concat('writing')hobbys:[...prevState.hobbys,'writing']}
})
- 截取选项: 使用 slice
this.setState({hobbys: this.state.hobbys.slice(0, 2)
})
- 插入选项: 使用
slice
克隆一份,然后用splice
插入选项
this.setState((prevState) => {let currentState = prevState.hobbys.slice() // 先克隆一份currentState.splice(1, 0, 'basketball')return {hobbys: currentState}
})
- 过滤选项: 使用
filter
this.setState({hobbys: this.state.hobbys.filter((item) => item.length < 5)
})
注意,不能直接使用 push pop splice shift unshift 等,因为这些方法都是在原数组的基础上修改,这样违反不可变值
见示例库里的 stateDemo4.js
5、setState 到底是异步还是同步?
Promise.then()
,setTimeout
是异步执行.,从 js
执行来说,setState
肯定是同步执行。
这里讨论的同步和异步并不是指 setState
是否异步执行,而是指调用 setState
之后 this.state
能否立即更新。
先给出答案:
- 在
legacy
模式中,即通过ReactDOM.render(<App />, rootNode)
创建的,在合成事件和生命周期函数里是异步的,在原生事件和setTimeout
、promise
等异步函数是同步的 - 在
blocking
模式中,即通过ReactDOM.createBlockingRoot(rootNode).render(<App />)
创建的,任何场景下setState
都是异步的 - 在
concurrent
模式中,即通过ReactDOM.createRoot(rootNode).render(<App />)
创建的,任何场景下setState
都是异步的
模式的说明详看官网
但由于后两种模式目前处于实验阶段,所以我们先重点分析下 legacy
模式,后面源码分析时,会说明下为什么其他两个模式都是异步的。
5.1 合成事件和生命周期函数里是异步的
import React, { Component } from 'react'export default class stateDemo5 extends Component {state = {count:0}componentDidMount() {this.setState({count:this.state.count+1})console.info("didMount count:",this.state.count) // didMount count: 0}handleChangeCount = () => {this.setState({count:this.state.count+1})console.info("update count:",this.state.count) // update count: 1}render() {return (<div>{this.state.count}<button onClick={this.handleChangeCount}>更改</button></div>)}
}
可以看到在 componentDidMount
生命周期函数与 handleChangeCount
合成事件里,setState
之后,获取到的 state
的值是旧值。
见示例库里的 stateDemo5.js
5.1.1、setState 合并处理
采用这种设置 state
方式,也会出现合并的现象:
import React, { Component } from 'react'export default class stateDemo6 extends Component {state = {count:0}handleChangeCount = () => {this.setState({count:this.state.count+1},() => {console.info("update count:",this.state.count)})this.setState({count:this.state.count+1},() => {console.info("update count:",this.state.count)})this.setState({count:this.state.count+1},() => {console.info("update count:",this.state.count)})}render() {return (<div>{this.state.count}<button onClick={this.handleChangeCount}>更改</button></div>)}
}
输出控制台信息如下:
update count: 1
update count: 1
update count: 1
本质上等同于 Object.assign
:
Object.assign(state,{count: state.count + 1},{count: state.count + 1},{count: state.count + 1})
即后面的对象会覆盖前面的,所以只有最后的 setState
才是有效
见示例库里的 stateDemo6.js
那么要怎么弄才不会合并呢?
将 setState
的第一个参数设置为函数形式:
import React, { Component } from 'react'export default class stateDemo7 extends Component {state = {count:0}handleChangeCount = () => {this.setState(prevState => {return {count:prevState.count+1}},() => {console.info("update count:",this.state.count)})this.setState(prevState => {return {count:prevState.count+1 }},() => {console.info("update count:",this.state.count)})this.setState(prevState => {return {count:prevState.count+1 }},() => {console.info("update count:",this.state.count)})}render() {return (<div>{this.state.count}<button onClick={this.handleChangeCount}>更改</button></div>)}
}
输出控制台信息如下:
update count: 3
update count: 3
update count: 3
函数式 setState
工作机制类似于:
[{increment: 1},{increment: 1},{increment: 1}
].reduce((prevState, props) => ({count: prevState.count + props.increment
}), {count: 0})
// {count: 3}
见示例库里的 stateDemo7.js
5.2 在原生事件和 setTimeout 里是同步的
import React, { Component } from 'react'export default class stateDemo8 extends Component {state = {count:0}componentDidMount() {document.querySelector("#change").addEventListener("click", () => {this.setState({count: this.state.count + 1,});console.log("update count1:", this.state.count); // update count1: 1});}handleChangeCount = () => {setTimeout(() => {this.setState({count: this.state.count + 1,});console.log("update count2:", this.state.count); // update count2: 1}, 0);}render() {return (<div><p>{this.state.count}</p><button id="change">更改1</button><button onClick={this.handleChangeCount}>更改2</button></div>)}
}
可以看到原生的事件(通过 addEventListener
绑定的),或者 setTimeout
等异步方式更改的 state
是同步的。
见示例库里的 stateDemo8.js
6、源码解读
网上的根据 isBatchingUpdates
变量的值来判断是同步还是异步的方式,实际上 react 16.8 之前的代码实现。
我这边是 React 17.0.1 源码
setState
内会调用this.updater.enqueueSetState
// packages/react/src/ReactBaseClasses.js
Component.prototype.setState = function (partialState, callback) {// 省略次要代码this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
- 在
enqueueSetState
方法中会创建update
并调度update
// packages/react-reconciler/src/ReactFiberClassComponent.old.js
enqueueSetState(inst, payload, callback) {// 通过组件实例获取对应fiberconst fiber = getInstance(inst);const eventTime = requestEventTime();const suspenseConfig = requestCurrentSuspenseConfig();// 获取优先级const lane = requestUpdateLane(fiber, suspenseConfig);// 创建updateconst update = createUpdate(eventTime, lane, suspenseConfig);update.payload = payload;// 赋值回调函数if (callback !== undefined && callback !== null) {update.callback = callback;}// 将update插入updateQueueenqueueUpdate(fiber, update);// 调度updatescheduleUpdateOnFiber(fiber, lane, eventTime);
}
- 在
scheduleUpdateOnFiber
方法中会根据lane
进行不同的处理(重点)
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function scheduleUpdateOnFiber(fiber, lane, eventTime) {// 省略与本次讨论无关代码if (lane === SyncLane) { // 同步任务if ( // 检查当前是不是在unbatchedUpdates(非批量更新),(初次渲染的ReactDOM.render就是unbatchedUpdates)(executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering(executionContext & (RenderContext | CommitContext)) === NoContext) {// Register pending interactions on the root to avoid losing traced interaction data.schedulePendingInteractions(root, lane); performSyncWorkOnRoot(root);} else {ensureRootIsScheduled(root, eventTime);schedulePendingInteractions(root, lane);if (executionContext === NoContext) {resetRenderTimer();flushSyncCallbackQueue();}}} else { // 异步任务// concurrent模式下是跳过了 flushSyncCallbackQueue 同步更新// ....} }
可以看出逻辑主要在判断 lane
executionContext
这两个变量。
lane
是由 requestUpdateLane
方法返回的:
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
export function requestUpdateLane(fiber: Fiber): Lane {// Special casesconst mode = fiber.mode;if ((mode & BlockingMode) === NoMode) {return (SyncLane: Lane);} else if ((mode & ConcurrentMode) === NoMode) {return getCurrentPriorityLevel() === ImmediateSchedulerPriority? (SyncLane: Lane): (SyncBatchedLane: Lane);}// 省略其他代码return lane;
}
可以看到首先判断模式:
如果是采用 legacy
模式,则返回 SyncLane
;
如果是采用 concurrent
,当优先级没达到立即执行时,则返回 SyncBatchedLane
,否则返回 SyncLane
接着说下 executionContext
变量:
每次触发事件都会调用 batchedEventUpdates$1
,而在这方法里会给 executionContext
赋值,并在执行完之后将 executionContext
还原
function batchedEventUpdates$1(fn, a) {var prevExecutionContext = executionContext;executionContext |= EventContext;try {return fn(a);} finally {executionContext = prevExecutionContext;if (executionContext === NoContext) {// Flush the immediate callbacks that were scheduled during this batchresetRenderTimer();flushSyncCallbackQueue();}}
}
所以:
如果是 concurrent
模式,由于并不会去判断 executionContext === NoContext
,所以不可能同步。
而在 legacy
模式下,当 executionContext === NoContext
时,就会同步,那么两者何时相等呢?
默认 executionContext
就是为 NoContext
,
而在 react 能管控到的范围,比如 batchedEventUpdates$1
方法里都会将 executionContext
设置为非 NoContext
,所以在合成事件和生命周期函数里是异步的。
但在 react 管控不到的,比如通过 addEventListener
绑定的事件,以及异步方法 setTimeout
就是同步的。
异步方法之所以是同步是由于当执行 setTimeout
后,react 会将 NoContext
还原,即上面的 finally 代码处理的,所以等到 setTimeout
回调函数执行时,executionContext
等于 NoContext
了。
根据上面的分析,大家应该可以很清晰的知道开头那个面试题目分别会输出什么了吧。
答案是: 0 0 2 3
见示例库里的 stateDemo
你真的了解 setState 吗?相关推荐
- React 源码剖析系列 - 解密 setState
this.setState() 方法应该是每一位使用 React 的同学最先熟悉的 API.然而,你真的了解 setState 么?先看看下面这个小问题,你能否正确回答. 引子 class Examp ...
- 从 Dropdown 的 React 实现中学习到的
Demo Demo Link Note dropdown 是一种很常见的 component,一般有两种: 展开 dropdown menu 后,点击任意地方都应该收起 menu. 展开 dropdo ...
- react textarea 空格为什么不换行_你需要的 React + TypeScript 50 条规范和经验
这篇文章没有对错之分,肯定也有不完善的地方,结合了自己日常开发和经验.可以让你书写代码更具严谨性,希望看完之后有所帮助.本文字数4000+ ,看完本文大概需半小时. 1. 注释 (1) 文件顶部的注释 ...
- react 显示当前时间_React 灵魂 23 问,你能答对几个?
1.setState 是异步还是同步? 合成事件中是异步 钩子函数中的是异步 原生事件中是同步 setTimeout中是同步 相关链接: 你真的理解setState吗? 2.聊聊 react@16.4 ...
- 一个优秀的前端都应该阅读这些文章
前言 的确,有些标题党了.起因是微信群里,有哥们问我,你是怎么学习前端的呢?能不能共享一下学习方法.一句话也挺触动我的,我真的不算是什么大佬,对于学习前端知识,我也不能说是掌握了什么捷径.当然,我个人 ...
- 【前端面试分享】- 寒冬求职上篇
前言 在这互联网的寒冬腊月时期,虽说过了金三银四,但依旧在招人不断.更偏向于招聘高级开发工程师.本人在这期间求职,去了几家创业,小厂,大厂厮杀了一番,也得到了自己满意的offer. 整理一下自己还记得 ...
- 这些年掘金上的优质前端文章,篇篇经典,一次打包带走!
前言:近日发现掘金上有所有的热门文章的排行榜,但是仅仅只是排行,不利于收藏查阅.于是乎我就把热门文章全部爬下来了(站长看到别打我啊?),相信这些获得高赞文章质量不会差,爬完做了分类后不敢私藏,和大家一 ...
- 优秀文章收藏(慢慢消化)持续更新~
better-learning 整理收藏一些优秀的文章及大佬博客留着慢慢学习 原文:https://www.ahwgs.cn/youxiuwenzhangshoucang.html github:ht ...
- 你真的弄明白了吗?Java并发之AQS详解
你真的弄明白了吗?Java并发之AQS详解 带着问题阅读 1.什么是AQS,它有什么作用,核心思想是什么 2.AQS中的独占锁和共享锁原理是什么,AQS提供的锁机制是公平锁还是非公平锁 3.AQS在J ...
最新文章
- Python id() 函数
- Jenkins项目迁移
- Magicodes.IE 2.5.5.3发布
- 数学家的浪漫,你想都想不到!
- quickServer介绍
- css中的 font 与 font-size
- C语言小游戏---扫雷
- 电脑小技巧:怎么取消电脑开机密码
- COMSOL Multiphysics 多物理场仿真学习小记
- 洛伦兹吸引子 matlab,使用Matplotlib画洛伦兹吸引子 | 学步园
- L1-6 烤地瓜 (15 分)
- 王者转号仅显示可转移的服务器,王者荣耀转移账号是免费的吗 角色转移进度怎么查看...
- 计算机系统基础实验 - 同符号浮点数加法运算/无符号定点数乘法运算的机器级表示
- python线性加权回归_第二十一章 regression算法——线性回归局部加权回归算法(上)...
- 51单片机使用LCD1602显示DS18B20温度传感器温度
- 区块链教程(2)——P2P交易原理
- 抓取检测之Closing the Loop for Robotic Grasping: A Real-time, Generative Grasp Synthesis Approach
- VR开发 入门 使用Three.js 开发的WebVR demo
- NullPointerException异常的原因及java异常
- html和sketch文件转换,GitHub - 332065255/sketch-to-html: 从 sketch 转换成 html,我开始更新了.....
热门文章
- 如何在所有主要浏览器中清除浏览器缓存(快速方式)
- gd mysql错误_php编译gd出错!(已解决)
- python3.8 三利器之 生成器
- 电子作业票系统:以“智能”拧紧危化安全生产“安全阀”
- 安全防护之Windows八大保密技巧
- 2017年下半年软件设计师选择题
- AutoMagic-开源自动化平台构建思路
- html锚点滑动效果,【转载】HTML锚点效果改进平滑移动页面滚动特效实现技术
- vue-路由的下载安装
- html当中的属性cellspacing,html中table标签之cellspacing属性的作用