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)keyref是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 Fiber


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM