了解 React 同窗想必对setState
函数是再熟悉不过了,setState
也会常常做为面试题,考察前端求职者对 React 的熟悉程度。html
在此我也抛一个问题,阅读文章前读者能够先想一下这个问题的答案。前端
给 React 组件的状态每次设置相同的值,如
setState({count: 1})
。React 组件是否会发生渲染?若是是,为何?若是不是,那又为何?react
针对上述问题,先进行一个简单的复现验证。git
如图所示,App 组件有个设置按钮,每次点击设置按钮,都会对当前组件的状态设置相同的值{count: 1}
,当组件发生渲染时渲染次数会自动累加一,代码以下所示:github
App 组件面试
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 全局变量,用于记录组件渲染次数
let renderTimes = 0;
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 1
};
}
handleClick = () => {
this.setState({ count: 1 });
};
render() {
renderTimes += 1;
return (
<div> <h3>场景复现:</h3> <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p> <p>当前组件的状态: {this.state.count}</p> <p> 当前组件发生渲染的次数: <span style={{ color: 'red' }}>{renderTimes}</span> </p> <div> <button onClick={this.handleClick}>设置</button> </div> </div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root')); 复制代码
实际验证结果以下所示,每次点击设置按钮,App 组件均会发生重复渲染。性能优化
那么该如何减小 App 组件发生重复渲染呢?以前在 React 性能优化——浅谈 PureComponent 组件与 memo 组件 一文中,详细介绍了PureComponent
的内部实现机制,此处可利用PureComponent
组件来减小重复渲染。less
实际验证结果以下所示,优化后的 App 组件再也不产生重复渲染。dom
但这有个细节问题,可能你们平时工做中并未想过:函数
利用
PureComponent
组件可减小 App 组件的重复渲染,那么是否表明 App 组件的状态没有发生变化呢?即引用地址是否依旧是上次地址呢?
废话很少说,咱们针对这一问题进行下测试验证,代码以下:
APP 组件
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
// 全局变量,用于记录组件渲染次数
let renderTimes = 0;
// 全局变量,记录组件的上次状态
let lastState = null;
class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
count: 1
};
lastState = this.state; // 初始化,地址保持一致
}
handleClick = () => {
console.log(`当前组件状态是不是上一次状态:${this.state === lastState}`);
this.setState({ count: 1 });
// 更新上一次状态
lastState = this.state;
};
render() {
renderTimes += 1;
return (
<div> <h3>场景复现:</h3> <p>每次点击“设置”按钮,当前组件的状态都会被设置成相同的数值。</p> <p>当前组件的状态: {this.state.count}</p> <p> 当前组件发生渲染的次数: <span style={{ color: 'red' }}>{renderTimes}</span> </p> <div> <button onClick={this.handleClick}>设置</button> </div> </div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root')); 复制代码
在 APP 组件中,咱们经过全局变量lastState
来记录组件的上次状态。当点击设置按钮时,会比较当前组件状态与上一次状态是否相等,即引用地址是否同样?
在 console 窗口中咱们发现,虽然 PureComponent
组件减小了 App 组件的重复渲染,可是 App 组件状态的引用地址却发生了变化,这是为何呢?
下面咱们将带着这两个疑问,结合 React V16.9.0 源码,聊一聊setState
的状态更新机制。解读过程当中为了更好的理解源码,会对源码存在部分删减。
在解读源码的过程当中,整理了一份函数setState
调用关系流程图,以下所示:
从上图能够看出,函数setState
调用关系主要分为如下两个部分:
下面针对这两个部分,结合源码,进行下详细阐述。
摘自ReactBaseClasses.js
文件。
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
复制代码
函数setState
包含两个参数partialState
和callback
,其中partialState
表示待更新的部分状态,callback
则为状态更新后的回调函数。
摘自ReactFiberClassComponent.js
文件。
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
// 建立一个update对象
const update = createUpdate(expirationTime, suspenseConfig);
// payload存放的是要更新的状态,即partialState
update.payload = payload;
// 若是定义了callback,则将callback挂载在update对象上
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// ...省略...
// 将update对象添加至更新队列中
enqueueUpdate(fiber, update);
// 添加调度任务
scheduleWork(fiber, expirationTime);
},
复制代码
函数enqueueSetState
会建立一个update
对象,并将要更新的状态partialState
、状态更新后的回调函数callback
和渲染的过时时间expirationTime
等都会挂载在该对象上。而后将该update
对象添加到更新队列中,而且产生一个调度任务。
若组件渲染以前屡次调用了setState
,则会产生多个update
对象,会被依次添加到更新队列中,同时也会产生多个调度任务。
摘自 ReactUpdateQueue.js
文件。
export function createUpdate( expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, ): Update<*> {
let update: Update<*> = {
expirationTime,
suspenseConfig,
// 添加TAG标识,表示当前操做是UpdateState,后续会用到。
tag: UpdateState,
payload: null,
callback: null,
next: null,
nextEffect: null,
};
return update;
}
复制代码
函数createUpdate
会建立一个update
对象,用于存放更新的状态partialState
、状态更新后的回调函数callback
和渲染的过时时间expirationTime
。
从上图能够看出,每次调用setState
函数都会建立一个调度任务。而后通过一系列函数调用,最终会调起函数updateClassComponent
。
图中红色区域涉及知识点较多,与咱们要讨论的状态更新机制关系不大,不是咱们这次的讨论重点,因此咱们先行跳过,待后续研究(挖坑)。
下面咱们就简单聊下组件实例的状态是如何一步步完成更新操做的。
摘自 ReactUpdateQueue.js
文件。
function getStateFromUpdate<State>( workInProgress: Fiber, queue: UpdateQueue<State>, update: Update<State>, prevState: State, nextProps: any, instance: any, ): any {
switch (update.tag) {
// ....省略 ....
// 见3.3节内容,调用setState会建立update对象,其属性tag当时被标记为UpdateState
case UpdateState: {
// payload 存放的是要更新的状态state
const payload = update.payload;
let partialState;
// 获取要更新的状态
if (typeof payload === 'function') {
partialState = payload.call(instance, prevState, nextProps);
} else {
partialState = payload;
}
// partialState 为null 或者 undefined,则视为未操做,返回上次状态
if (partialState === null || partialState === undefined) {
return prevState;
}
// 注意:此处经过Object.assign生成一个全新的状态state, state的引用地址发生了变化。
return Object.assign({}, prevState, partialState);
}
// .... 省略 ....
}
return prevState;
}
复制代码
getStateFromUpdate
函数主要功能是将存储在更新对象update
上的partialState
与上一次的prevState
进行对象合并,生成一个全新的状态 state。
注意:
Object.assign
第一个参数是空对象,也就是说新的 state 对象的引用地址发生了变化。Object.assign
进行的是浅拷贝,不是深拷贝。摘自 ReactUpdateQueue.js
文件。
export function processUpdateQueue<State>( workInProgress: Fiber, queue: UpdateQueue<State>, props: any, instance: any, renderExpirationTime: ExpirationTime, ): void {
// ...省略...
// 获取上次状态prevState
let newBaseState = queue.baseState;
/** * 若在render以前屡次调用了setState,则会产生多个update对象。这些update对象会以链表的形式存在queue中。 * 如今对这个更新队列进行依次遍历,并计算出最终要更新的状态state。 */
let update = queue.firstUpdate;
let resultState = newBaseState;
while (update !== null) {
// ...省略...
/** * resultState做为参数prevState传入getStateFromUpdate,而后getStateFromUpdate会合并生成 * 新的状态再次赋值给resultState。完成整个循环遍历,resultState即为最终要更新的state。 */
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
// ...省略...
// 遍历下一个update对象
update = update.next;
}
// ...省略...
// 将处理后的resultState更新到workInProgess上
workInProgress.memoizedState = resultState;
}
复制代码
React 组件渲染以前,咱们一般会屡次调用setState
,每次调用setState
都会产生一个 update 对象。这些 update 对象会以链表的形式存在队列 queue 中。processUpdateQueue
函数会对这个队列进行依次遍历,每次遍历会将上一次的prevState
与 update 对象的partialState
进行合并,当完成全部遍历后,就能算出最终要更新的状态 state,此时会将其存储在 workInProgress 的memoizedState
属性上。
摘自 ReactFiberClassComponent.js
文件。
function updateClassInstance( current: Fiber, workInProgress: Fiber, ctor: any, newProps: any, renderExpirationTime: ExpirationTime, ): boolean {
// 获取当前实例
const instance = workInProgress.stateNode;
// ...省略...
const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
let updateQueue = workInProgress.updateQueue;
// 若是更新队列不为空,则处理更新队列,并将最终要更新的state赋值给newState
if (updateQueue !== null) {
processUpdateQueue(
workInProgress,
updateQueue,
newProps,
instance,
renderExpirationTime,
);
newState = workInProgress.memoizedState;
}
// ...省略...
/** * shouldUpdate用于标识组件是否要进行渲染,其值取决于组件的shouldComponentUpdate生命周期执行结果, * 亦或者PureComponent的浅比较的返回结果。 */
const shouldUpdate = checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
);
if (shouldUpdate) {
// 若是须要更新,则执行相应的生命周期函数
if (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
typeof instance.componentWillUpdate === 'function') {
startPhaseTimer(workInProgress, 'componentWillUpdate');
if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(newProps, newState, nextContext);
}
if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
}
stopPhaseTimer();
}
// ...省略...
}
// ...省略...
/** * 无论shouldUpdate的值是true仍是false,都会更新当前组件实例的props和state的值, * 即组件实例的state和props的引用地址发生变化。也就是说即便咱们采用PureComponent来减小无用渲染, * 但并不表明该组件的state或者props的引用地址没有发生变化!!! */
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
复制代码
从上述代码能够看出,updateClassInstance
函数主要实现了如下几个功能:
shouldUpdate
,该值的运行结果取决于shouldComponentUpdate
生命周期函数执行结果或者PureComponent
的浅比较结果;shouldUpdate
的值为true
,则执行相应生命周期函数componentWillUpdate
;此时要特别注意如下几点:
PureComponent
或者shouldComponentUpdate
来减小无用渲染,但组件实例的 props 或者 state 的引用地址也依旧发生了变化。代码解读到此处,想必你们对以前提到的两个疑问都有了答案吧。
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// 获取组件实例
const instance = workInProgress.stateNode;
// ...省略...
let shouldUpdate;
/** * 1. 完成组件实例的state、props的更新; * 2. componentWillUpdate、shouldComponentUpdate生命周期函数执行完毕; * 3. 获取是否要进行更新的标识shouldUpdate; */
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
/** * 1. 若是shouldUpdate值为false,则退出渲染; * 2. 执行render函数 */
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
// 返回下一个任务单元
return nextUnitOfWork;
}
复制代码
从上述代码能够看出,updateClassComponent
函数主要实现了如下几个功能:
componentWillUpdate
、shouldComponentUpdate
等生命周期函数;通过上章的代码解读,相信你们应该对函数setState
应该有了全新的认识。以前提到的两个疑问,应该都有了本身的答案。在此我简单小结一下:
每次调用函数setState
,react 都会将要更新的状态添加到更新队列中,并产生一个调度任务。调度任务在执行的过程当中会作两个事情:
shouldUpdate
来决定是否对组件实例进行从新渲染,而标识shouldUpdate
的值则取决于PureComponent
组件浅比较结果或者生命周期函数shouldComponentUpdate
执行结果;利用PureComponent
组件能够减小组件实例的重复渲染,但组件实例的状态因为被赋予了一个全新的状态,因此引用地址发生了变化。
文章就暂时写到这了,若是你们以为博文还不错,那就帮忙点个赞吧。
其余: