React 源碼版本: v16.9.0
源碼註釋筆記:airingursb/react
1 類組件 VS. 函數組件#
根據 React 官網,React 中的組件可分為函數式組件(Functional Component)與類組件(Class Component)。
1.1 類組件#
這是一個我們熟悉的類組件:
// Class Componment
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
1.2 函數組件#
而函數式組件則更加簡潔:
// Functional Componment
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
1.3 無狀態組件#
而函數式組件在以往我們也稱其為無狀態組件(Stateless Component),因為在函數組件中,我們無法使用state
;甚至它也沒法使用組件的生命週期方法。一個函數組件只負責接收props
,渲染 DOM,而不去關注其他邏輯。
這類組件有以下幾個特點:
- 只負責接收
props
,渲染 DOM - 沒有
state
- 不能訪問生命週期方法
- 不需要聲明類:可以避免 extends 或 constructor 之類的代碼,語法上更加簡潔。
- 不會被實例化:因此不能直接傳 ref(可以使用
React.forwardRef
包裝後再傳 ref)。 - 不需要顯示聲明 this 關鍵字:在 ES6 的類聲明中往往需要將函數的 this 關鍵字綁定到當前作用域,而因為函數式聲明的特性,我們不需要再強制綁定。
- 更好的性能表現:因為函數式組件中並不需要進行生命週期的管理與狀態管理,因此 React 並不需要進行某些特定的檢查或者內存分配,從而保證了更好地性能表現。
無狀態組件的代碼更加簡單清晰且易於快速實現,它們適用於非常小的 UI 界面,即這些組件的重新渲染的成本很小。
2. 類組件 VS. 純組件#
2.1 類組件#
生命週期函數 shouldComponentUpdate
返回一個布爾值:
- true: 那麼當
props
或者state
改變的時候進行更新 - false: 不更新
在普通的 Class Component 中該生命週期函數默認返回 true,也就是那麼當 props
或者 state
改變的時候類組件及其子組件會進行更新。
2.2 純組件#
基於函數式編程範例中純度的概念,如果符合以下兩個條件,那麼我們可以稱一個組件是 Pure Component:
- 其返回值僅由其輸入值決定
- 對於相同的輸入值,返回值始終相同。
如果 React 組件為相同的 state 和 props 呈現相同的輸出,則可以將其視為純組件。對於像這樣的類組件,React 提供了 PureComponent 基類。基於React.PureComponent
類實現的的類組件被視為純組件。
Pure Component 可以減少不必要的更新,進而提升性能,每次更新會自動幫你對更新前後的 props 和 state 進行一個簡單對比,來決定是否進行更新。
接下來我們看看源碼。在入口文件 React.js 中暴露了 Component 和 PureComponent 兩個基類,它們來自於 packages/react/src/ReactBaseClasses.js:
首先是基本的 Component:
/**
* 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.
// 不同平台 updater 不一樣
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
// 略
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
然後是 PureComponent:
/**
* 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;
可以看到 PureComponent 完全繼承自 Component,只是在原型鏈上加了一個
isPureReactComponent
,那麼這個isPureReactComponent
有什麼用?
在調度更新的時候,這個屬性會用來檢查組件是否需要更新
// packages/react-reconciler/src/ReactFiberClassComponent.js
function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
const instance = workInProgress.stateNode;
if (typeof instance.shouldComponentUpdate === 'function') {
startPhaseTimer(workInProgress, 'shouldComponentUpdate');
const shouldUpdate = instance.shouldComponentUpdate(
newProps,
newState,
nextContext,
);
stopPhaseTimer();
return shouldUpdate;
}
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
return true;
}
如果是 PureComponent,那麼就會對新舊 props
和 state
用 shallowEqual()
進行淺比較。
那我們看看 shallowEqual()
做了些什麼:
import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* 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 {
// 透過 is 函數對兩個參數進行比較,判斷是否相同,相同直接返回true:基本數據類型值相同,同一個引用對象都表示相同
if (is(objA, objB)) {
return true;
}
// 如果兩個參數不相同,判斷兩個參數是否至少有一個不是引用類型,是即返回false if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 如果兩個都是引用類型對象,則繼續下面的比較
// 判斷兩個不同引用類型對象是否相同
// 先透過 Object.keys 獲取到兩個對象的所有屬性,具有相同屬性,且每個屬性值相同即兩個對相同(相同也透過is函數完成)
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;
// Object.is,排除了===兩種不符合預期的情況:
// 1. +0 === -0 // true
// 2. NaN === NaN // false
function is(x, y) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
由上面的分析可以看到,當對比的類型為 Object 並且 key 的長度相等的時候,淺比較也僅僅是用Object.is()
對 Object 的 value 做了一個基本數據類型的比較。因此如果 key 裡面是對象的話,有可能出現比較不符合預期的情況,所以淺比較是不適用於嵌套類型的比較的。
純組件對 React 的性能優化有重要意義,它減少了組件的渲染操作次數:如果一個組件是一個純組件,如果輸入沒有變動,那麼這個組件就不需要重新渲染。若組件樹越大,純組件帶來的性能優化收益就越高。
2.3 純函數組件#
在 1.2 和 1.3 中我們說明了無狀態的函數組件多麼好用,現在 Pure Component 也有性能上減少重複渲染的優點,那它們可以結合使用嗎,函數組件能否控制渲染?表面上看不行的,因為 Pure Component 就是一個類組件,它和函數組件的實現上風馬牛不相及。
但在 React 16.6 中提供了一個 memo
函數,它可以讓我們的函數組件也具備渲染控制的能力。React.memo()
是一個更高階的組件,接受一個函數組件,返回一個特殊的 HOC(Higher-Order Component),具有記憶功能,能記住輸出時渲染的組件。(關於 React.memo()
的實現以後有機會再寫一篇文章單獨說一下。)
import React, { memo } from 'react';
const ToTheMoonComponent = React.memo(function MyComponent(props) {
// only renders if props have changed
});
memo 是 memoization 的簡寫,備忘錄是一種優化技術,主要用於透過存儲昂貴的函數調用的結果來加速計算機程序,並在再次發生相同的輸入時返回緩存的結果。而這恰恰是 React.memo()
所做的實現,它會檢查即將到來的渲染是否和前一個相同,如果相同就保留不渲染。
在以前版本中,這個函數的名字叫 pure
,由 recompose
包提供,而不是 React 自帶的函數。
Memoized component. There was a lot of feedback on the RFC which we agree with — "pure" naming is confusing because memoization (which is what memo does) has nothing to do with function "purity". —— Dan Abramov
3 小節#
介紹了無狀態組件、函數組件、純組件、類組件之後,最後再來介紹一下選用 React 組件的 Keep it Simple Stupid (KISS) 原則:
- 如果組件不需要狀態,則使用無狀態組件
- 儘可能使用純組件
- 性能上:無狀態函數組件 > class components > React.createClass ()
- 最小化 props (接口):不要傳遞超過要求的 props
- 如果組件內部存在較多條件控制流,這通常意味著需要對組件進行抽取。
- 不要過早優化,只要求組件在當前需求下可被復用,然後隨機應變
這一節總結了一些 React 中組件的分類,還有 Smark Component 和 Dumb Component 的分類方法,但是這種主要是業務上的分類和技術原理無關所以就不說了。下一篇文章中將說一下這些組件的復用方法,以此說明我們為什麼需要 React Hooks :)