了解 React 同窗想必对setState
函数是再熟悉不过了,setState
也会常常做为面试题,考察前端求职者对 React 的熟悉程度。html
在此我也抛一个问题,阅读文章前读者能够先想一下这个问题的答案。前端
给 React 组件的状态每次设置相同的值,如
setState({count: 1})
。React 组件是否会发生渲染?若是是,为何?若是不是,那又为何?
针对上述问题,先进行一个简单的复现验证。react
如图所示,App 组件有个设置按钮,每次点击设置按钮,都会对当前组件的状态设置相同的值{count: 1}
,当组件发生渲染时渲染次数会自动累加一,代码以下所示:git
App 组件github
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
组件来减小重复渲染。性能优化
实际验证结果以下所示,优化后的 App 组件再也不产生重复渲染。less
但这有个细节问题,可能你们平时工做中并未想过: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
组件能够减小组件实例的重复渲染,但组件实例的状态因为被赋予了一个全新的状态,因此引用地址发生了变化。
文章就暂时写到这了,若是你们以为博文还不错,那就帮忙点个赞吧。
其余: