React可大體分爲三部分:Core、Reconciler和Renderer,在閱讀源碼以前,首先須要搭建測試環境,爲了方便起見,本文直接採用了網友搭建好的環境,React版本是16.8.6,與最新版本很接近。html
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。算法
在項目中引入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)key和ref是React組件中的兩個特殊屬性,前者用於標識身份,後者用於訪問render()方法內生成的組件實例和DOM元素。
(4)props是ReactElement中的屬性,包括特殊的children屬性。
雖然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 Fiber 那些事: 深刻解析新的協調算法
原文出處:https://www.cnblogs.com/strick/p/11950520.html