哈嘍大家好,好久沒有跟大家以技術文章的形式見面了。最近本人在使用 React 18 做 Web 專案,所以抽空研究了一下 React 18 的原始碼。接下來想做一個 React 18 原始碼分析的系列,系列文章會以「 demo + 原始碼 」的形式由淺入深地跟大家一起探討新版本的 React 的技術實現,歡迎點贊 / 關注 / 拍磚,一起進步
本系列將預設使用 React v18.1.0 版本,預設執行環境為瀏覽器,讀者可自行到 GitHub 下載 React 原始碼,敬請留意
本章我們將探討 React 專案初始化的時候做了哪些事情:
Demo
我們使用 create-react-app 這個官方腳手架建立一個 React 專案,然後將 index.js
這個檔案修改為以下程式碼
import { createRoot } from 'react-dom/client';
function App() {
return <h1>Hello dan!!!</h1>
}
const root = createRoot(document.getElementById('root'))
root.render(<App></App>)
執行 npm start
這個指令碼,如果你看到這個非常簡單(醜陋)的頁面顯示出來,那證明專案已經可以正常執行起來了
函式分析
從 Demo 我們看到,整個專案先通過 createRoot
這個函式建立一個 root
物件,再通過 root
的 render
方法將 App
這個元件渲染到網頁上
createRoot
我們先看 createRoot
這個方法具體做了什麼事情。這個方法來自 react-dom
這個包。我們可以在原始碼中 packages / react-dom / src / client / ReactDOMRoot.js
中找到 createRoot
的具體實現(前面在 ReactDOM.js 做了一些關於環境的條件判斷,可先忽略)
createRoot
函式有兩個引數 container
和 options
,其中 options
是可選引數,本章為了簡單起見先不討論;
該函式大概實現的功能就是:
- 建立容器物件
FiberRootNode
- 事件委託處理
- 根據
FiberRootNode
物件返回ReactDOMRoot
物件
// 刪除了一些干擾邏輯之後,createRoot 函式大致如下所示
function createRoot(
container: Element | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
// 建立容器物件 `FiberRootNode`
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
// 事件監聽處理
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
// 根據容器物件 `root` 返回 `ReactDOMRoot` 物件
return new ReactDOMRoot(root);
}
createContainer
函式完成了 建立容器物件 FiberRootNode
的工作。
這個方法來自 react-reconciler
這個包,可以在 packages / react-reconciler / src / ReactFiberReconciler.old.js
中找到,而這個方法內容也很簡單,直接呼叫了同層級 ReactFiberRoot.old.js
檔案的 createFiberRoot
方法來建立並返回一個 FiberRootNode
物件,也稱之為 Fiber 根結點
。
注意: 這裡的 Fiber 根結點
跟 Fiber 節點
是有區別的,詳細可看下面的各自的定義函式
這裡引入一個概念叫做 Fiber,目前我們只需要對他有個初步的印象:Fiber 節點用於儲存 React 元件節點資訊(包括 DOM節點,元件的屬性 / state / effect 等)。這裡可以簡單理解為一個儲存資訊的 JS 物件,後續章節會詳細介紹
function createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride,
identifierPrefix, onRecoverableError, transitionCallbacks) {
// 建立 FiberRootNode 物件
// tag 值為 ConcurrentRoot,定義在 packages/react-reconciler/src/ReactRootTags.js 檔案中;
// tag === ConcurrentRoot === 1 ,表示 “根節點”
var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
// 建議看到這裡先彆著急看後面的程式碼,先看看下面 FiberRootNode 的定義和建構函式
// 分割線 ************** 分割線 ************** 分割線 ************** 分割線 **************
// 看完 FiberRootNode 的定義之後,接下來馬上要建立 Fiber 物件
// createHostRootFiber 會呼叫 packages/react-reconciler/src/ReactFiber.old.js 檔案中的 createFiber 方法建立一個 `Fiber HostRoot節點`
// `Fiber HostRoot節點` 就是一個 Fiber 物件,只是他的 Tag 等於 3,代表 `HostRoot`
var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
// 把 `Fiber 根結點` 的 current 屬性指向剛建立的 `Fiber HostRoot節點`
root.current = uninitializedFiber;
// `Fiber HostRoot節點` 的 stateNode 屬性指向 `Fiber 根結點`
uninitializedFiber.stateNode = root;
// cache 相關的可先忽略
var initialCache = createCache();
retainCache(initialCache);
root.pooledCache = initialCache;
retainCache(initialCache);
// 初始化一個 state 物件
var initialState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
transitions: null
};
uninitializedFiber.memoizedState = initialState;
// 初始化 `Fiber HostRoot節點` 的更新佇列
// 給 Fiber 的 updateQueue 屬性賦值
/**
var queue = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes
},
effects: null
};
fiber.updateQueue = queue;
**/
initializeUpdateQueue(uninitializedFiber);
// 返回 `Fiber 根結點`
return root;
}
FiberRootNode
的定義:
一個建構函式,物件內儲存Fiber 根節點
的資訊,可先關注以下幾個
- tag:標識節點型別,此處為
ConcurrentRoot
- containerInfo:
Fiber 根節點
的 DOM 資訊,表示在這個 DOM 節點內部渲染當前 React 應用 - current:儲存當前 Fiber 樹(後續章節會講到)
其他屬性可以先大致掃一遍,重要是的後續會逐個介紹
// 在`packages/react-reconciler/src/ReactFiberRoot.old.js`檔案中 // FiberRootNode 建構函式 function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; this.current = null; .... // 省略其他屬性初始化 ....
Fiber 節點的定義
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// tag 表示 Fiber 型別
// packages/react-reconciler/src/ReactWorkTags.js 中定義
this.tag = tag;
// 寫在 jsx 元件上的 key 屬性
this.key = key;
// createElement的第一個引數,ReactElement 上的 type
this.elementType = null;
// 暫時可認為與 elementType 基本一致
this.type = null;
// fiber 節點對應的 DOM 節點
this.stateNode = null;
// Fiber 結構
// 指向父節點
this.return = null;
// 指向第一個子節點
this.child = null;
// 指向兄弟節點
this.sibling = null;
// 一般如果沒有兄弟節點的話是 0 當某個父節點下的子節點是陣列型別的時候會給每個子節點一個 index
this.index = 0;
// 儲存 ref 屬性物件
this.ref = null;
// 新的 props 物件
this.pendingProps = pendingProps;
// 現有的 props 物件
this.memoizedProps = null;
// 儲存更新物件的佇列
this.updateQueue = null;
// 現有的 state 物件
this.memoizedState = null;
// 依賴物件
this.dependencies = null;
// 渲染方式
// React 18 預設是 `ConcurrentMode`: 0b000001
// packages/react-reconciler/src/ReactTypeOfMode.js 檔案中定義
this.mode = mode;
// Effects
// effect 的 Flag,表明當前的 effect 是`替換`/ `更新` / `刪除` 等操作
// packages/react-reconciler/src/ReactFiberFlags.js
this.flags = NoFlags;
// 子樹的 Flag 合集
this.subtreeFlags = NoFlags;
// 需要刪除的 fiber 節點
this.deletions = null;
// 更新渲染排程優先順序相關
// packages/react-reconciler/src/ReactFiberLane.old.js 檔案中定義
this.lanes = NoLanes;
this.childLanes = NoLanes;
// current 樹和 workInprogress 樹之間的相互引用
// current 樹就是當前的 Fiber 樹
// workInprogress 樹 就是正在更新的 Fiber 樹
// 後續講到元件更新會詳細講到
this.alternate = null;
if (enableProfilerTimer) {
// 。。。 省略
}
}
總結一下: createContainer
方法通過 createFiberRoot
建立並返回 Fiber 根節點
:FiberRootNode
物件。同時該物件的 current 屬性指向一個 Fiber HostRoot節點
。
markContainerAsRoot
方法在容器 DOM 節點上新增一個屬性 __reactContainer${randomKey}
,屬性的值指向Fiber HostRoot節點
。以表明該 DOM 節點為當前 React 應用的容器節點。
listenToAllSupportedEvents
函式完成了 事件委託處理 的工作
在 packages/react-dom/src/events/DOMPluginEventSystem.js
檔案中,listenToAllSupportedEvents
函式接收一個入參:容器 DOM 節點(也就是createRoot
函式的第一個引數)
大致的原理是:React 18 把所有事件都委託到這個節點上面,一旦原生事件觸發之後,這個節點會根據事件型別以及優先順序,觸發對應 fiber 節點上的事件回撥函式。目前可以先了解一下 React 合成事件,後續章節講到事件機制會詳細講解
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (!(rootContainerElement: any)[listeningMarker]) {
(rootContainerElement: any)[listeningMarker] = true;
// allNativeEvents 是一個集合,儲存了 React 支援的所有事件
// Set(81) {'abort', 'auxclick', 'cancel', 'canplay', 'canplaythrough', …}
allNativeEvents.forEach(domEventName => {
// 這裡最重要的函式就是 `listenToNativeEvent`
// 用於將事件繫結到容器的 DOM 節點
// 下面會根據是否響應捕獲階段分邏輯處理(可先忽略)
// selectionchange 事件也單獨處理(可先忽略)
// listenToNativeEvent 事件內部呼叫 addTrappedEventListener 函式
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
// 。。。省略 selectionchange 邏輯
}
}
addTrappedEventListener
函式主要實現:根據事件獲取對應的優先順序,不同的優先順序在容器 DOM節點
註冊不同的事件回撥函式
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
// packages/react-reconciler/src/ReactEventPriorities.js 檔案儲存事件優先順序的定義
// createEventListenerWrapperWithPriority邏輯 :
// 1. 呼叫`getEventPriority` 函式實現從`事件名`到 `事件優先順序` 的轉化
// 2. 根據 `事件優先順序` eventPriority 匹配不同的回撥函式:(dispatchDiscreteEvent,dispatchContinuousEvent, dispatchEvent)
// 3. 返回事件回撥函式,賦值給 listener
var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); // If passive option is not supported, then the event will be
var isPassiveListener = undefined;
if (passiveBrowserEventsSupported) {
// 邏輯省略
}
targetContainer = targetContainer;
var unsubscribeListener;
// 事件繫結邏輯:
// 呼叫 addEventCaptureListener(WithPassiveFlag) / addEventBubbleListener((WithPassiveFlag)) 函式進行事件繫結,
// 內部呼叫原生方法 dom.addEventListener,實現事件繫結
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
}
} else {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
} else {
unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
}
}
}
總結一下:listenToAllSupportedEvents
函式根據不同的事件型別,給容器 DOM節點
註冊不同的回撥函式,子元件的所有事件都由該節點進行分發和觸發
返回 ReactDOMRoot 物件
例項化 ReactDOMRoot
物件,將 Fiber HostRoot節點
傳人建構函式中,儲存在物件的 _internalRoot
屬性
// ReactDOMRoot 建構函式
// 比較簡單,不解釋
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
createRoot
函式最後返回 ReactDOMRoot
物件,完成整個函式的所有工作。接下來,呼叫ReactDOMRoot
物件的 render
方法進行渲染工作
render
render
方法在 packages/react-dom/src/client/ReactDOMRoot.js
檔案中實現,入參是子元件,函式內部呼叫了 updateContainer
方法對子元件(App)進行渲染
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
const container = root.containerInfo;
// 重要步驟,重點分析
updateContainer(children, root, null, null);
};
updateContainer
函式在 packages/react-reconciler/src/ReactFiberReconciler.old.js
檔案中定義,主要實現容器的排程任務
Lane 在 React 中用於表示任務的優先順序,目前只需要有個大概的瞭解,後續會詳細講解
schedule 是一個獨立的任務排程模組,目前只用於 React 內部,很多 API 還處於 unstable 狀態,後續有可能會提供給外部專案使用;這個模組也會在後續單獨講解,敬請期待
// 刪除一些干擾邏輯之後的 `updateContainer` 函式
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// 當前的 Fiber 樹
const current = container.current;
// 當前事件時間,呼叫 `now` 函式
const eventTime = requestEventTime();
// 獲取當前更新的 lane (任務排程優先順序)
const lane = requestUpdateLane(current);
// 獲取上下文
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
// 將 FiberRootNode 的 context 屬性指向 context
container.context = context;
} else {
// 將 FiberRootNode 新的 context 屬性指向 context
container.pendingContext = context;
}
// 建立一個`更新物件`:update
/* var update = {
eventTime: eventTime, // 事件時間
lane: lane, // 排程優先順序
tag: UpdateState, // 標識是 update / delete / ForceUpdate / ...
payload: null, // payload,儲存 { element: React.element }
callback: null, // 回撥函式
next: null // 指向下一個 update
};
*/
const update = createUpdate(eventTime, lane);
// 設定更新物件的 paylaod 屬性
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 把`更新物件` enqueue 到`更新佇列`
// 後續在將元件更新的時候會細講,這塊還是比較重要的,目前可以大概瞭解
enqueueUpdate(current, update, lane);
// scheduleUpdateOnFiber 利用到 scheduler 這個包來進行任務排程
// 通過將渲染方法 performConcurrentWorkOnRoot 註冊到 scheduler 的排程機制中
// scheduler 會根據任務優先順序執行這個渲染方法,將 APP 元件最終渲染到頁面上
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
流程圖
至此,整個 React 專案的初始化過程就完成了,為了保證本章內容足夠簡單,很多細節都還沒有深入講解。不過,相信讀者讀完本章後,對整個初始化的過程也有了一定的瞭解。在後續的章節中,我們將針對 React 專案的其他階段進行深入剖析。同時也歡迎各位讀者可以在下方給我留言一起交流,共同進步
最後最後,希望疫情能夠早日結束,中國加油,世界加油 !!!