React躬行記(16)——React源碼分析

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

1、目錄結構

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

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

  (2)packages,React庫提供的包的源碼,包括核心代碼、矢量圖形庫等,以下所列。git

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

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

  React使用的前端模塊化打包工具是Rollup,在源碼中還引入了Flow,用於靜態類型檢查,在運行代碼以前發現一些潛在的問題,其語法相似於TypeScript。算法

2、React核心對象

  在項目中引入React一般是像下面這樣。json

import React from 'react';

  其實引入的是核心入口文件「packages/react/index.js」中導出的對象,以下所示,其中React.default用於Jest測試,React用於Rollup。segmentfault

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__是個全局變量,用於管理開發環境中運行的代碼塊。api

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屬性

3、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中。

4、生命週期鉤子方法

  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

原文出處:https://www.cnblogs.com/strick/p/11950520.html

相關文章
相關標籤/搜索