Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

React Hooks 源碼解析(1):類組件、函數組件、純組件

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,而不去關注其他邏輯。

這類組件有以下幾個特點:

  1. 只負責接收props,渲染 DOM
  2. 沒有 state
  3. 不能訪問生命週期方法
  4. 不需要聲明類:可以避免 extends 或 constructor 之類的代碼,語法上更加簡潔。
  5. 不會被實例化:因此不能直接傳 ref(可以使用 React.forwardRef 包裝後再傳 ref)。
  6. 不需要顯示聲明 this 關鍵字:在 ES6 的類聲明中往往需要將函數的 this 關鍵字綁定到當前作用域,而因為函數式聲明的特性,我們不需要再強制綁定。
  7. 更好的性能表現:因為函數式組件中並不需要進行生命週期的管理與狀態管理,因此 React 並不需要進行某些特定的檢查或者內存分配,從而保證了更好地性能表現。

無狀態組件的代碼更加簡單清晰且易於快速實現,它們適用於非常小的 UI 界面,即這些組件的重新渲染的成本很小。

2. 類組件 VS. 純組件#

2.1 類組件#

生命週期函數 shouldComponentUpdate 返回一個布爾值:

  • true: 那麼當 props 或者 state 改變的時候進行更新
  • false: 不更新

在普通的 Class Component 中該生命週期函數默認返回 true,也就是那麼當 props 或者 state 改變的時候類組件及其子組件會進行更新。

2.2 純組件#

基於函數式編程範例中純度的概念,如果符合以下兩個條件,那麼我們可以稱一個組件是 Pure Component:

  1. 其返回值僅由其輸入值決定
  2. 對於相同的輸入值,返回值始終相同。

如果 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,那麼就會對新舊 propsstateshallowEqual()進行淺比較。

那我們看看 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 :)

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。