瞭解 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
組件能夠減小組件實例的重複渲染,但組件實例的狀態因爲被賦予了一個全新的狀態,因此引用地址發生了變化。
文章就暫時寫到這了,若是你們以爲博文還不錯,那就幫忙點個贊吧。
其餘: