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