從原始碼看React.PureComponent
React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.
React.PureComponent 和 React.Component 幾乎相同,區別在於 React.PureComponent 會淺比較 props、state是否發生變化從而決定是否更新元件(這裡的淺比較在後面的原始碼分析中會提到)
使用 React.PureComponent 也是React應用優化的一種方式,當然也能使用 React.Component 定義shouldComponentUpdate
生命週期函式來實現一樣的功能,但是直接使用 React.PureComponent 能更加直觀和簡便
看一個簡單的例子:
使用React.Component
class CounterButton extends React.Component { state = { count: 1 } shouldComponentUpdate(nextProps, nextState) { if (this.props.color !== nextProps.color) { return true; } if (this.state.count !== nextState.count) { return true; } return false; } render() { return ( <button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button> ); } } 複製程式碼
使用React.PureComponent
class CounterButton extends React.PureComponent { state = { count: 1 } render() { return ( <button color={this.props.color} onClick={() => this.setState(state => ({count: state.count + 1}))}> Count: {this.state.count} </button> ); } } 複製程式碼
上面兩段程式碼都能避免不必要的元件更新,優化效能
原始碼
Component & PureComponent 定義
ReactBaseClasses.js
const emptyObject = {}; /** * Base class helpers for the updating state of a component. */ function Component(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } Component.prototype.isReactComponent = {}; Component.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; Component.prototype.forceUpdate = function(callback) { this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); }; function ComponentDummy() {} ComponentDummy.prototype = Component.prototype; /** * Convenience component with default shallow equality check for sCU. */ function PureComponent(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; } const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); pureComponentPrototype.constructor = PureComponent; // Avoid an extra prototype jump for these methods. Object.assign(pureComponentPrototype, Component.prototype); pureComponentPrototype.isPureReactComponent = true; export {Component, PureComponent}; 複製程式碼
從原始碼來看,Component 和 PureComponent 基本一樣,唯一區別在於 PureComponent 定義了isPureReactComponent
為true
,這是為了方便在React應用執行過程中區分 Component 和 PureComponent
在分析後續的原始碼之前,建議小夥伴去看下我的文章:ofollow,noindex">React16原始碼之React Fiber架構 ,這篇文章分析了React應用整體的執行流程
本文重點分析 reconciliation階段beginWork
函式中的updateClassComponent
函式的呼叫(這一部分在React16原始碼之React Fiber架構
中重點分析了)
beginWork
函式主要有兩部分工作:
1、對Context進行處理
2、根據Fiber節點的tag型別,呼叫對應的update方法
而tag型別為ClassComponent
的Fiber節點會呼叫updateClassComponent
函式,我們來看看updateClassComponent
函式的核心原始碼
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) { ... let shouldUpdate; if (current === null) { if (workInProgress.stateNode === null) { // In the initial pass we might need to construct the instance. constructClassInstance( workInProgress, Component, nextProps, renderExpirationTime, ); mountClassInstance( workInProgress, Component, nextProps, renderExpirationTime, ); shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, Component, nextProps, renderExpirationTime, ); } } else { shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderExpirationTime, ); } return finishClassComponent( current, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime, ); } 複製程式碼
執行流程如下:
current為null,表示當前元件第一次渲染
判斷當前元件是否需要初始化
-
workInProgress.stateNode === null
表示需要初始化,呼叫constructClassInstance
、mountClassInstance
兩個函式 -
否則,表示元件已初始化,則呼叫
resumeMountClassInstance
函式複用初始化過的例項
(React原始碼也在不斷更新,所以這塊邏輯比React16原始碼之React Fiber架構 講的邏輯多了一個複用邏輯)
current不為null,呼叫updateClassInstance
constructClassInstance
、mountClassInstance
做的工作:
-
constructClassInstance
主要是初始化元件例項,即呼叫constructor
建構函式,並注入classComponentUpdater
-
mountClassInstance
則是呼叫getDerivedStateFromProps
生命週期函式(v16)及UNSAFE_componentWillMount
生命週期函式
從上面的原始碼可以看到,resumeMountClassInstance
函式和updateClassInstance
函式都會將返回值賦值給shouldUpdate
變數,而shouldUpdate
變數是布林型別,在後面的流程中,決定是否執行render
函式
這裡以updateClassInstance
函式為例來看看原始碼
function updateClassInstance( current: Fiber, workInProgress: Fiber, ctor: any, newProps: any, renderExpirationTime: ExpirationTime, ): boolean { // 如果新老props不一致,則會呼叫 UNSAFE_componentWillReceiveProps 生命週期函式 ... let updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { processUpdateQueue( workInProgress, updateQueue, newProps, instance, renderExpirationTime, ); newState = workInProgress.memoizedState; } // 執行 getDerivedStateFromProps 生命週期函式 ... const shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextLegacyContext, ); if (shouldUpdate) { ... } else { ... } ... return shouldUpdate; } 複製程式碼
重點關注checkShouldComponentUpdate
函式
function checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextLegacyContext, ) { const instance = workInProgress.stateNode; if (typeof instance.shouldComponentUpdate === 'function') { startPhaseTimer(workInProgress, 'shouldComponentUpdate'); const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, nextLegacyContext, ); stopPhaseTimer(); return shouldUpdate; } if (ctor.prototype && ctor.prototype.isPureReactComponent) { return ( !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } return true; } 複製程式碼
執行流程如下:
1、是否有shouldComponentUpdate
生命週期函式,有則呼叫此生命週期函式並返回結果(shouldUpdate
)
2、判斷此元件是否為PureComponent
,是則執行shallowEqual
對新老props、新老state進行淺比較,並返回比較結果
3、預設返回true
shallowEqual
函式:
const hasOwnProperty = Object.prototype.hasOwnProperty; function is(x, y) { // SameValue algorithm if (x === y) { // Steps 1-5, 7-10 // Steps 6.b-6.e: +0 != -0 // Added the nonzero y check to make Flow happy, but it is redundant return x !== 0 || y !== 0 || 1 / x === 1 / y; } else { // Step 6.a: NaN == NaN return x !== x && y !== y; } } /** * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */ function shallowEqual(objA: mixed, objB: mixed): boolean { if (is(objA, objB)) { return true; } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } } return true; } export default shallowEqual; 複製程式碼
可以看到,shallowEqual
真的就是淺比較,所以對於props、state是複雜資料結構如果使用 PureComponent 往往會導致更新問題
當props、state是簡單資料結構的元件適合使用 PureComponent,或者使用 forceUpdate() 來更新複雜資料結構,或者考慮結合immutable objects 使用,或者直接使用 Component,自定義shouldComponentUpdate
生命週期函式
說到forceUpdate()
可以順便看下原始碼,首先看看forceUpdate
函式定義,在前面也說過在給元件初始化時,會給元件例項注入classComponentUpdater
,而呼叫forceUpdate
其實就是呼叫classComponentUpdater.enqueueForceUpdate
,來看看定義
const classComponentUpdater = { ... enqueueForceUpdate(inst, callback) { ... const update = createUpdate(expirationTime); // !!! update.tag = ForceUpdate; if (callback !== undefined && callback !== null) { update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }, }; 複製程式碼
可以看到,在將update放入佇列之前,執行了update.tag = ForceUpdate;
,這個標記將在後面用於標識更新是否為ForceUpdate
,後面的流程與正常更新流程一直,可以參考React16原始碼之React Fiber架構
我們再回到updateClassInstance
函式,在執行checkShouldComponentUpdate
函式之前,執行了processUpdateQueue
函式及進行了checkHasForceUpdateAfterProcessing
函式判斷
processUpdateQueue
函式主要是遍歷updateQueue
,呼叫getStateFromUpdate
函式
getStateFromUpdate
函式原始碼如下:
function getStateFromUpdate<State>( workInProgress: Fiber, queue: UpdateQueue<State>, update: Update<State>, prevState: State, nextProps: any, instance: any, ): any { switch (update.tag) { case ReplaceState: { ... } case CaptureUpdate: { ... } // Intentional fallthrough case UpdateState: { ... } case ForceUpdate: { hasForceUpdate = true; return prevState; } } return prevState; } 複製程式碼
我們可以看到,此函式是判斷update的tag型別,對於ForceUpdate
型別會將hasForceUpdate
變數設定為true
checkHasForceUpdateAfterProcessing
函式則是返回hasForceUpdate
變數,程式碼如下:
export function checkHasForceUpdateAfterProcessing(): boolean { return hasForceUpdate; } 複製程式碼
當呼叫了forceUpdate
函式,無論是否存在shouldComponentUpdate
生命週期函式,無論此元件是否為 PureComponent,都會強制更新,所以應該謹慎使用