这是我参与 8 月更文挑战的第 3 天,活动详情查看: 8月更文挑战javascript
本文全部示例html
setState 算是 React 里被使用的最高频的 api,但你真的了解 setState 吗?好比下面这段代码,你能清楚的知道输出什么吗?java
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
的异步更新,另外输出结果也要看当前处于哪一种模式下。react
咱们先从 setState
的用法提及,以便全面掌握git
虽然咱们一直在用 setState
,可有没想过为何 React 里会有该 api
?github
React 是经过管理状态来实现对组件的管理,即 UI = f(state)
f 就是咱们的代码,最主要的就是 this.setState
,调用该函数后 React 会使用更新的 state
从新渲染此组件及其子组件,即达到了 UI 层的变动。web
setState
是 React 官方提供的更新 state
的方法,经过调用 setState
,React 会使用最新的 state 值,并调用 render
方法将变化展示到视图。面试
在 React v16.3 版本以前,调用 setState
方法会依次触发如下生命周期函数api
那么 state 在哪一个生命周期里会更新为最新的值?数组
import React, { Component } from 'react'
export default class stateDemo2 extends Component {
state = {
count: 0
}
shouldComponentUpdate() {
console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0
return 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 1
return (
<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
方法会依次触发如下生命周期函数
确切的说,应该是 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 1
return { ...state }
}
shouldComponentUpdate() {
console.info('shouldComponentUpdate', this.state.count) // shouldComponentUpdate 0
return true
}
increase = () => {
this.setState({
count: this.state.count + 1
})
}
render() {
console.info('render', this.state.count) //render 1
return (
<div> <p>{this.state.count}</p> <button onClick={this.increase}>累加</button> </div>
)
}
getSnapshotBeforeUpdate() {
console.info('getSnapshotBeforeUpdate', this.state.count) //getSnapshotBeforeUpdate 1
return null
}
componentDidUpdate() {
console.info('componentDidUpdate', this.state.count) //componentDidUpdate 1
}
}
复制代码
能够看到新增的两个生命周期函数 getDerivedStateFromProps
与 getSnapshotBeforeUpdate
获取到的 state
都是新值
见示例库里的 stateDemo3.js
第一个参数是一个对象,会将传入的对象浅层合并到 ;第二个参数是个可选的回调函数
例如,调整购物车商品数:
this.setState({quantity: 2})
复制代码
在回调函数参数里,能够获取到最新的 state
值,但推荐使用 componentDidUpdate
第一个参数是个函数,(state, props) => stateChange,第二个参数同上是个可选的回调函数
例如:
this.setState((state, props) => {
return {counter: state.counter + props.step};
});
复制代码
updater 函数中接收的 state 和 props 都保证为最新。updater 的返回值会与 state 进行浅合并。
咱们要严格遵行 state
是不可变的原则,即不能够直接修改 state
变量,例如底下的作法就是不可取的
this.state.count ++
this.state.count ++
this.setState({})
复制代码
这样是实现了同步更改 state
的目的,但违背了 state
是不可变的原则
this.setState({
count: 1,
name: 'zhangsan',
flag: true
})
复制代码
使用 ES6 的 Object.assign
或解构赋值
this.setState({
// person: Object.assign({}, this.state.person, { name: 'lisi' })
person:{...this.state.person,age:22}
})
复制代码
见示例库里的 stateDemo4.js
concat
或者解构赋值this.setState((prevState) => {
return {
// hobbys: prevState.hobbys.concat('writing')
hobbys:[...prevState.hobbys,'writing']
}
})
复制代码
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
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
模式,后面源码分析时,会说明下为何其余两个模式都是异步的。
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
采用这种设置 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
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
网上的根据 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) {
// 经过组件实例获取对应fiber
const fiber = getInstance(inst);
const eventTime = requestEventTime();
const suspenseConfig = requestCurrentSuspenseConfig();
// 获取优先级
const lane = requestUpdateLane(fiber, suspenseConfig);
// 建立update
const update = createUpdate(eventTime, lane, suspenseConfig);
update.payload = payload;
// 赋值回调函数
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 将update插入updateQueue
enqueueUpdate(fiber, update);
// 调度update
scheduleUpdateOnFiber(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 cases
const 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 batch
resetRenderTimer();
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