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