1. 程式人生 > >React躬行記(16)——React原始碼分析

React躬行記(16)——React原始碼分析

  React可大致分為三部分:Core、Reconciler和Renderer,在閱讀原始碼之前,首先需要搭建測試環境,為了方便起見,本文直接採用了網友搭建好的環境,React版本是16.8.6,與最新版本很接近。

一、目錄結構

  React採用了由Lerna維護monorepo方式進行程式碼管理,即用一個倉庫管理多個模組(module)或包(package)。在React倉庫的根目錄中,包含三個目錄:

  (1)fixtures,給原始碼貢獻者準備的測試用例。

  (2)packages,React庫提供的包的原始碼,包括核心程式碼、向量圖形庫等,如下所列。

├── packages ------------------------------------ 原始碼目錄
│   ├── react-art ------------------------------- 向量圖形渲染器
│   ├── react-dom ------------------------------- DOM渲染器
│   ├── react-native-renderer ------------------- Native渲染器(原生iOS和Android檢視)
│   ├── react-test-renderer --------------------- JSON樹渲染器
│   ├── react-reconciler ------------------------ React調和器

  (3)scripts,相關的工具配置指令碼,包括語法規則、Git鉤子等。

  React使用的前端模組化打包工具是Rollup,在原始碼中還引入了Flow,用於靜態型別檢查,在執行程式碼之前發現一些潛在的問題,其語法類似於TypeScript。

二、React核心物件

  在專案中引入React通常是像下面這樣。

import React from 'react';

  其實引入的是核心入口檔案“packages/react/index.js”中匯出的物件,如下所示,其中React.default用於Jest測試,React用於Rollup。

const React = require('./src/React');
// TODO: decide on the top-level export form.
// This is hacky but makes it work with both Rollup and Jest.
module.exports = React.default || React;

  順著require()語句可以找到React.js中的React物件,程式碼省略了一大堆匯入語句,其中__DEV__是個全域性變數,用於管理開發環境中執行的程式碼塊。

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,
  lazy,
  memo,

  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useReducer,
  useRef,
  useState,

  Fragment: REACT_FRAGMENT_TYPE,
  Profiler: REACT_PROFILER_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,
  unstable_SuspenseList: REACT_SUSPENSE_LIST_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  unstable_withSuspenseConfig: withSuspenseConfig,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};

if (enableFlareAPI) {
  React.unstable_useResponder = useResponder;
  React.unstable_createResponder = createResponder;
}
if (enableFundamentalAPI) {
  React.unstable_createFundamental = createFundamental;
}
if (enableJSXTransformAPI) {
  if (__DEV__) {
    React.jsxDEV = jsxWithValidation;
    React.jsx = jsxWithValidationDynamic;
    React.jsxs = jsxWithValidationStatic;
  } else {
    React.jsx = jsx;
    React.jsxs = jsx;
  }
}
export default React;

  在React物件中包含了開放的核心API,例如React.Component、React.createRef()等,以及新引入的Hooks(內部的具體邏輯可轉移到相關的包中),但渲染的邏輯已經剝離出來。

1)React.createElement()

  JSX中的元素稱為React元素,分為兩種型別:DOM元素和元件元素。用JSX描述的元件都會通過Babel編譯器將它們轉換成React.createElement()方法,它包含三個引數(如下所示),其中type是元素型別,也就是它的名稱;props是一個由元素屬性組成的物件;children是它的子元素(即內容),可以是文字也可以是其它元素。

React.createElement(type, [props], [...children])

  方法的返回值是一個ReactElement,省略了開發環境中的程式碼。

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner        //記錄建立該元素的元件
  };
  return element;
};

  (1)$$typeof標識該物件是一個ReactElement。

  (2)當ReactElement是DOM元素時,type是元素名稱;當ReactElement是元件元素時,type是其建構函式。

  (3)key和ref是React元件中的兩個特殊屬性,前者用於標識身份,後者用於訪問render()方法內生成的元件例項和DOM元素。

  (4)props是ReactElement中的屬性,包括特殊的children屬性。

三、Reconciler

  雖然React的DOM和Native兩種渲染器內部實現的區別很大,但為了能共享自定義元件、State、生命週期等特性,做到跨平臺,就需要共享一些邏輯,而這些邏輯由Reconciler統一處理,其中協調演算法(Diffing演算法)也要儘可能相似。

1)Diffing演算法

  當呼叫React的render()方法時,會建立一棵由React元素組成的樹。在下一次State或Props更新時,相同的render()方法會返回一棵不同的樹。React會應用Diffing演算法來高效的比較兩棵樹,演算法過程如下。

  (1)當根節點為不同型別的元素時,React會拆卸原有的樹,銷燬對應的DOM節點和關聯的State、解除安裝子元件,最後再建立新的樹。

  (2)當比對兩個相同型別的DOM元素時,會保留DOM節點,僅比對變更的屬性。

  (3)當比對兩個相同型別的元件元素時,元件例項保持不變,更新該元件例項的Props。

  (4)當遞迴DOM節點的子元素時,React會同時遍歷兩個子元素的列表,比對相同位置的元素,效能比較低效。

  (5)在給子元素新增唯一標識的key屬性後,就能只比對變更了key屬性的元素。

2)Fiber Reconciler

  JavaScript與樣式計算、介面佈局等各種繪製,一起執行在瀏覽器的主執行緒中,當JavaScript執行時間過長時,將佔用整個執行緒,阻塞其它任務。為了能在React渲染期間回到主執行緒執行其它任務,在React v16中提出了Fiber Reconciler,並將其設為預設的Reconciler,解決了過去Stack Reconciler中的固有問題和遺留的痛點,提高了動畫、佈局和手勢等領域的效能。Fiber Reconciler的主要目標是:

  (1)暫停和切分渲染任務,並將分割的任務分佈到各個幀中。

  (2)調整優先順序,並重置或複用已完成的任務。

  (3)在父子元素之間交錯處理,以支援React中的佈局。

  (4)在render()方法中返回多個元素。

  (5)更好地支援錯誤邊界。

3)排程任務

  Fiber可以分解任務,根據優先順序將任務排程到瀏覽器提供的兩個全域性函式中,如下所列。

  (1)requestAnimationFrame:在下一個動畫幀上執行高優先順序的任務。

  (2)requestIdleCallback:線上程空閒時執行低優先順序的任務。

   當網頁保持在每秒60幀(1幀約為16ms)時,整體會變得很流暢。在每個幀中呼叫requestAnimationFrame()執行高優先順序的任務;而在兩個幀之間會有一小段空閒時間,此時可執行requestIdleCallback()中的任務,該函式包含一個deadline引數(截止時間),用於切分長任務。

4)Fiber資料結構

  在調和期間,從render()方法得到的每個React元素都需要升級為Fiber節點,並新增到Fiber節點樹中。而與React元素不同,Fiber節點可複用,不會在每次渲染時重新建立。Fiber的資料結構大致如下,省略了部分屬性,原始碼來自於packages/react-reconciler/src/ReactFiber.js。

export type Fiber = {
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
  effectTag: SideEffectTag,
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,
  expirationTime: ExpirationTime,
  alternate: Fiber | null,
  ...
};

  return、child和sibling三個屬性分別表示父節點、第一個子節點和兄弟節點,通過它們使得Fiber節點能夠基於連結串列連線在一起。假設有個ClickCounter元件,包含<button>和<span>兩個元素,它們三者之間的關係如圖12所示。

class ClickCounter extends React.Component {
  render() {
    return [
      <button>Update counter</button>,
      <span>10</span>
    ];
  }
}

圖 12 節點關係

  使用alternate屬性雙向連線當前Fiber和正在處理的Fiber(workInProgress),如下程式碼所示,當需要恢復時,可通過alternate屬性直接回退。

let workInProgress = current.alternate;
if (workInProgress === null) {
  workInProgress.alternate = current;
  current.alternate = workInProgress;
}

  到期時間(ExpirationTime)是指完成此任務的時間,該時間越短,則優先順序越高,需要儘早執行,具體邏輯在同目錄的ReactFiberExpirationTime.js中。

四、生命週期鉤子方法

  React在內部執行時會分為兩個階段:render和commit。

  在第一個render階段(phase)中,React持有標記了副作用(side effect)的Fiber樹並將其應用於例項,該階段不會發生使用者可見的更改,並且可非同步執行,下面列出的是在render階段執行的生命週期鉤子方法

  (1)[UNSAFE_]componentWillMount(棄用)

  (2)[UNSAFE_]componentWillReceiveProps(棄用)

  (3)getDerivedStateFromProps

  (4)shouldComponentUpdate

  (5)[UNSAFE_]componentWillUpdate(棄用)

  (6)render

  標有UNSAFE的生命週期有可能被執行多次,並且經常被誤解和濫用,例如在這些方法中執行副作用程式碼,可能出現渲染問題,或者任意操作DOM,可能引起迴流(reflow)。於是官方推出了靜態的getDerivedStateFromProps()方法,可限制狀態更新以及DOM操作。

  在第二個commit階段,任務都是同步執行的,下面列出的是commit階段執行的生命週期鉤子方法,這些方法都只執行一次,其中getSnapshotBeforeUpdate()是新增的,用於替換componentWillUpdate()。

  (1)getSnapshotBeforeUpdate

  (2)componentDidMount

  (3)componentDidUpdate

  (4)componentWillUnmount

  新的流程將變成圖13這樣。

圖 13 新的流程

 

【參考資料】
原始碼概覽 官網

貢獻者說明

React 原始碼解析系列(jokcy)

如何閱讀大型前端開源專案的原始碼

React原始碼解析(邏輯圖)

react原始碼學習環境搭建

React原始碼系列(一): 總結看原始碼心得及方法感受

React原始碼分析系列

react原始碼開始的那一步

React 原始碼全方位剖析

「譯」React Fiber 那些事: 深入解析新的協調演算法

【翻譯】React Fiber 架構

React Fiber架構

為 Luy 實現 React Fiber 架構

協調 官網

完全理解React F