引言
在上一篇文章中我們通過create-react-app腳手架快速搭建了一個簡單的示例,并基于該示例講解了在類組件中React.Component和React.PureComponent背后的實作原理,同時我們也了解到,通過使用Babel預置工具包@babel/preset-react可以將類組件中render方法的回傳值和函式定義組件中的回傳值轉換成使用React.createElement方法包裝而成的多層嵌套結構,并基于原始碼逐行分析了React.createElement方法背后的實作程序和ReactElement建構式的成員結構,最后根據分析結果總結出了幾道面試中可能會碰到或者自己以前遇到過的面試考點,上篇文章中的內容相對而言還是比較簡單基礎,主要是為本文以及后續的任務調度相關內容打下基礎,幫助我們更好地理解原始碼的用意,本文就結合上篇文章的基礎內容,從組件渲染的入口點ReactDOM.render方法開始,一步一步深入原始碼,揭秘ReactDOM.render方法背后的實作原理,如有錯誤,還請指出,
原始碼中有很多判斷類似__DEV__變數的控制陳述句,用于區分開發環境和生產環境,筆者在閱讀原始碼的程序中不太關心這些內容,就直接略過了,有興趣的小伙伴兒可以自己研究研究,
render VS hydrate
本系列的原始碼分析是基于Reactv16.10.2版本的,為了保證原始碼一致還是建議你選擇相同的版本,下載該版本的地址和筆者選擇該版本的具體原因可以在上篇文章的準備階段小節中查看,這里就不做過多講解了,專案示例本身也比較簡單,可以按照準備階段的步驟自行使用create-react-app快速將一個簡單的示例搭建起來,然后我們定位到src/index.js檔案下,可以看到如下代碼:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
...
ReactDOM.render(<App />, document.getElementById('root'));
...
該檔案即為專案的主入口檔案,App組件即為根組件,ReactDOM.render就是我們要開始分析原始碼的入口點,我們通過以下路徑可以找到ReactDOM物件的完整代碼:
packages -> react-dom -> src -> client -> ReactDOM.js
然后我們將代碼定位到第632行,可以看到ReactDOM物件包含了很多我們可能使用過的方法,例如render、createPortal、findDOMNode,hydrate和unmountComponentAtNode等,本文中我們暫且只關心render方法,但為了方便對比,也可以簡單看下hydrate方法:
const ReactDOM: Object = {
...
/**
* 服務端渲染
* @param element 表示一個ReactNode,可以是一個ReactElement物件
* @param container 需要將組件掛載到頁面中的DOM容器
* @param callback 渲染完成后需要執行的回呼函式
*/
hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
...
// TODO: throw or warn if we couldn't hydrate?
// 注意第一個引數為null,第四個引數為true
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
true,
callback,
);
},
/**
* 客戶端渲染
* @param element 表示一個ReactElement物件
* @param container 需要將組件掛載到頁面中的DOM容器
* @param callback 渲染完成后需要執行的回呼函式
*/
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
...
// 注意第一個引數為null,第四個引數為false
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
...
};
發現沒,render方法的第一個引數就是我們在上篇文章中講過的ReactElement物件,所以說上篇文章的內容就是為了在這里打下基礎的,便于我們對引數的理解,事實上,在原始碼中幾乎所有方法引數中的element欄位均可以傳入一個ReactElement實體,這個實體就是通過Babel編譯器在編譯程序中使用React.createElement方法得到的,接下來在render方法中呼叫legacyRenderSubtreeIntoContainer來正式進入渲染流程,不過這里需要留意一下的是,render方法和hydrate方法在執行legacyRenderSubtreeIntoContainer時,第一個引數的值均為null,第四個引數的值恰好相反,
然后將代碼定位到第570行,進入legacyRenderSubtreeIntoContainer方法的具體實作:
/**
* 開始構建FiberRoot和RootFiber,之后開始執行更新任務
* @param parentComponent 父組件,可以把它當成null值來處理
* @param children ReactDOM.render()或者ReactDOM.hydrate()中的第一個引數,可以理解為根組件
* @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二個引數,組件需要掛載的DOM容器
* @param forceHydrate 表示是否融合,用于區分客戶端渲染和服務端渲染,render方法傳false,hydrate方法傳true
* @param callback ReactDOM.render()或者ReactDOM.hydrate()中的第三個引數,組件渲染完成后需要執行的回呼函式
* @returns {*}
*/
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
...
// TODO: Without `any` type, Flow says "Property cannot be accessed on any
// member of intersection type." Whyyyyyy.
// 在第一次執行的時候,container上是肯定沒有_reactRootContainer屬性的
// 所以第一次執行時,root肯定為undefined
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// Initial mount
// 首次掛載,進入當前流程控制中,container._reactRootContainer指向一個ReactSyncRoot實體
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
// root表示一個ReactSyncRoot實體,實體中有一個_internalRoot方法指向一個fiberRoot實體
fiberRoot = root._internalRoot;
// callback表示ReactDOM.render()或者ReactDOM.hydrate()中的第三個引數
// 重寫callback,通過fiberRoot去找到其對應的rootFiber,然后將rootFiber的第一個child的stateNode作為callback中的this指向
// 一般情況下我們很少去寫第三個引數,所以可以不必關心這里的內容
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
// 對于首次掛載來說,更新操作不應該是批量的,所以會先執行unbatchedUpdates方法
// 該方法中會將executionContext(執行背景關系)切換成LegacyUnbatchedContext(非批量背景關系)
// 切換背景關系之后再呼叫updateContainer執行更新操作
// 執行完updateContainer之后再將executionContext恢復到之前的狀態
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 不是首次掛載,即container._reactRootContainer上已經存在一個ReactSyncRoot實體
fiberRoot = root._internalRoot;
// 下面的控制陳述句和上面的邏輯保持一致
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
// 對于非首次掛載來說,是不需要再呼叫unbatchedUpdates方法的
// 即不再需要將executionContext(執行背景關系)切換成LegacyUnbatchedContext(非批量背景關系)
// 而是直接呼叫updateContainer執行更新操作
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
上面代碼的內容稍微有些多,咋一看可能不太好理解,我們暫且可以不用著急看完整個函式內容,試想當我們第一次啟動運行專案的時候,也就是第一次執行ReactDOM.render方法的時候,這時去獲取container._reactRootContainer肯定是沒有值的,所以我們先關心第一個if陳述句中的內容:
if (!root) {
// Initial mount
// 首次掛載,進入當前流程控制中,container._reactRootContainer指向一個ReactSyncRoot實體
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
...
}
這里通過呼叫legacyCreateRootFromDOMContainer方法將其回傳值賦值給container._reactRootContainer,我們將代碼定位到同檔案下的第517行,去看看legacyCreateRootFromDOMContainer的具體實作:
/**
* 創建并回傳一個ReactSyncRoot實體
* @param container ReactDOM.render()或者ReactDOM.hydrate()中的第二個引數,組件需要掛載的DOM容器
* @param forceHydrate 是否需要強制融合,render方法傳false,hydrate方法傳true
* @returns {ReactSyncRoot}
*/
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactSyncRoot {
// 判斷是否需要融合
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
// 針對客戶端渲染的情況,需要將container容器中的所有元素移除
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 回圈遍歷每個子節點進行洗掉
while ((rootSibling = container.lastChild)) {
...
container.removeChild(rootSibling);
}
}
...
// Legacy roots are not batched.
// 回傳一個ReactSyncRoot實體
// 該實體具有一個_internalRoot屬性指向fiberRoot
return new ReactSyncRoot(
container,
LegacyRoot,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
/**
* 根據nodeType和attribute判斷是否需要融合
* @param container DOM容器
* @returns {boolean}
*/
function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(
rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
);
}
/**
* 根據container來獲取DOM容器中的第一個子節點
* @param container DOM容器
* @returns {*}
*/
function getReactRootElementInContainer(container: any) {
if (!container) {
return null;
}
if (container.nodeType === DOCUMENT_NODE) {
return container.documentElement;
} else {
return container.firstChild;
}
}
其中在shouldHydrateDueToLegacyHeuristic方法中,首先根據container來獲取DOM容器中的第一個子節點,獲取該子節點的目的在于通過節點的nodeType和是否具有ROOT_ATTRIBUTE_NAME屬性來區分是客戶端渲染還是服務端渲染,ROOT_ATTRIBUTE_NAME位于packages/react-dom/src/shared/DOMProperty.js檔案中,表示data-reactroot屬性,我們知道,在服務端渲染中有別于客戶端渲染的是,node服務會在后臺先根據匹配到的路由生成完整的HTML字串,然后再將HTML字串發送到瀏覽器端,最終生成的HTML結構簡化后如下:
<body>
<div id="root">
<div data-reactroot=""></div>
</div>
</body>
在客戶端渲染中是沒有data-reactroot屬性的,因此就可以區分出客戶端渲染和服務端渲染,在React中的nodeType主要包含了五種,其對應的值和W3C中的nodeType標準是保持一致的,位于與DOMProperty.js同級的HTMLNodeType.js檔案中:
// 代表元素節點
export const ELEMENT_NODE = 1;
// 代表文本節點
export const TEXT_NODE = 3;
// 代表注釋節點
export const COMMENT_NODE = 8;
// 代表整個檔案,即document
export const DOCUMENT_NODE = 9;
// 代表檔案片段節點
export const DOCUMENT_FRAGMENT_NODE = 11;
經過以上分析,現在我們就可以很容易地區分出客戶端渲染和服務端渲染,并且在面試中如果被問到兩種渲染模式的區別,我們就可以很輕松地在原始碼級別上說出兩者的實作差異,讓面試官眼前一亮,怎么樣,到目前為止,其實還是覺得挺簡單的吧?
FiberRoot VS RootFiber
在這一小節中,我們將嘗試去理解兩個比較容易混淆的概念:FiberRoot和RootFiber,這兩個概念在React的整個任務調度程序中起著關鍵性的作用,如果不理解這兩個概念,后續的任務調度程序就是空談,所以這里也是我們必須要去理解的部分,接下來接著上一小節的內容,繼續分析legacyCreateRootFromDOMContainer方法中的剩余內容,在函式體的結尾回傳了一個ReactSyncRoot實體,我們重新回到ReactDOM.js檔案可以很容易找到ReactSyncRoot建構式的具體內容:
/**
* ReactSyncRoot建構式
* @param container DOM容器
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @param options 配置資訊,只有在hydrate時才有值,否則為undefined
* @constructor
*/
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}
/**
* 創建并回傳一個fiberRoot
* @param container DOM容器
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @param options 配置資訊,只有在hydrate時才有值,否則為undefined
* @returns {*}
*/
function createRootImpl(
container: DOMContainer,
tag: RootTag,
options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
// 判斷是否是hydrate模式
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
// 創建一個fiberRoot
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
// 給container附加一個內部屬性用于指向fiberRoot的current屬性對應的rootFiber節點
markContainerAsRoot(root.current, container);
if (hydrate && tag !== LegacyRoot) {
const doc =
container.nodeType === DOCUMENT_NODE
? container
: container.ownerDocument;
eagerlyTrapReplayableEvents(doc);
}
return root;
}
從上述原始碼中,我們可以看到createRootImpl方法通過呼叫createContainer方法來創建一個fiberRoot實體,并將該實體回傳并賦值到ReactSyncRoot建構式的內部成員_internalRoot屬性上,我們繼續深入createContainer方法去探究一下fiberRoot完整的創建程序,該方法被抽取到與react-dom包同級的另一個相關的依賴包react-reconciler包中,然后定位到react-reconciler/src/ReactFiberReconciler.js的第299行:
/**
* 內部呼叫createFiberRoot方法回傳一個fiberRoot實體
* @param containerInfo DOM容器
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @param hydrate 判斷是否是hydrate模式
* @param hydrationCallbacks 只有在hydrate模式時才可能有值,該物件包含兩個可選的方法:onHydrated和onDeleted
* @returns {FiberRoot}
*/
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
/**
* 創建fiberRoot和rootFiber并相互參考
* @param containerInfo DOM容器
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @param hydrate 判斷是否是hydrate模式
* @param hydrationCallbacks 只有在hydrate模式時才可能有值,該物件包含兩個可選的方法:onHydrated和onDeleted
* @returns {FiberRoot}
*/
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
// 通過FiberRootNode建構式創建一個fiberRoot實體
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
// 通過createHostRootFiber方法創建fiber tree的根節點,即rootFiber
// 需要留意的是,fiber節點也會像DOM樹結構一樣形成一個fiber tree單鏈表樹結構
// 每個DOM節點或者組件都會生成一個與之對應的fiber節點(生成的程序會在后續的文章中進行解讀)
// 在后續的調和(reconciliation)階段起著至關重要的作用
const uninitializedFiber = createHostRootFiber(tag);
// 創建完rootFiber之后,會將fiberRoot實體的current屬性指向剛創建的rootFiber
root.current = uninitializedFiber;
// 同時rootFiber的stateNode屬性會指向fiberRoot實體,形成相互參考
uninitializedFiber.stateNode = root;
// 最后將創建的fiberRoot實體回傳
return root;
}
一個完整的FiberRootNode實體包含了很多有用的屬性,這些屬性在任務調度階段都發揮著各自的作用,可以在ReactFiberRoot.js檔案中看到完整的FiberRootNode建構式的實作(這里只列舉部分屬性):
/**
* FiberRootNode建構式
* @param containerInfo DOM容器
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @param hydrate 判斷是否是hydrate模式
* @constructor
*/
function FiberRootNode(containerInfo, tag, hydrate) {
// 用于標記fiberRoot的型別
this.tag = tag;
// 指向當前激活的與之對應的rootFiber節點
this.current = null;
// 和fiberRoot關聯的DOM容器的相關資訊
this.containerInfo = containerInfo;
...
// 當前的fiberRoot是否處于hydrate模式
this.hydrate = hydrate;
...
// 每個fiberRoot實體上都只會維護一個任務,該任務保存在callbackNode屬性中
this.callbackNode = null;
// 當前任務的優先級
this.callbackPriority = NoPriority;
...
}
部分屬性資訊如上所示,由于屬性過多并且在本文中暫時還用不到,這里就先不一一列舉出來了,剩余的屬性及其注釋資訊已經上傳至Github,感興趣的朋友可以自行查看,在了解完了fiberRoot的屬性結構之后,接下來繼續探究createFiberRoot方法的后半部分內容:
// 以下代碼來自上文中的createFiberRoot方法
// 通過createHostRootFiber方法創建fiber tree的根節點,即rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 創建完rootFiber之后,會將fiberRoot實體的current屬性指向剛創建的rootFiber
root.current = uninitializedFiber;
// 同時rootFiber的stateNode屬性會指向fiberRoot實體,形成相互參考
uninitializedFiber.stateNode = root;
// 以下代碼來自ReactFiber.js檔案
/**
* 內部呼叫createFiber方法創建一個FiberNode實體
* @param tag fiberRoot節點的標記(LegacyRoot、BatchedRoot、ConcurrentRoot)
* @returns {Fiber}
*/
export function createHostRootFiber(tag: RootTag): Fiber {
let mode;
// 以下代碼根據fiberRoot的標記型別來動態設定rootFiber的mode屬性
// export const NoMode = 0b0000; => 0
// export const StrictMode = 0b0001; => 1
// export const BatchedMode = 0b0010; => 2
// export const ConcurrentMode = 0b0100; => 4
// export const ProfileMode = 0b1000; => 8
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BatchedMode | StrictMode;
} else if (tag === BatchedRoot) {
mode = BatchedMode | StrictMode;
} else {
mode = NoMode;
}
...
// 呼叫createFiber方法創建并回傳一個FiberNode實體
// HostRoot表示fiber tree的根節點
// 其他標記型別可以在shared/ReactWorkTags.js檔案中找到
return createFiber(HostRoot, null, null, mode);
}
/**
* 創建并回傳一個FiberNode實體
* @param tag 用于標記fiber節點的型別(所有的型別存放在shared/ReactWorkTags.js檔案中)
* @param pendingProps 表示待處理的props資料
* @param key 用于唯一標識一個fiber節點(特別在一些串列資料結構中,一般會要求為每個DOM節點或組件加上額外的key屬性,在后續的調和階段會派上用場)
* @param mode 表示fiber節點的模式
* @returns {FiberNode}
*/
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
// FiberNode建構式用于創建一個FiberNode實體,即一個fiber節點
return new FiberNode(tag, pendingProps, key, mode);
};
至此我們就成功地創建了一個fiber節點,上文中我們提到過,和DOM樹結構類似,fiber節點也會形成一個與DOM樹結構對應的fiber tree,并且是基于單鏈表的樹結構,我們在上面剛創建的fiber節點可作為整個fiber tree的根節點,即RootFiber節點,在目前階段,我們暫時不用關心一個fiber節點所包含的所有屬性,但可以稍微留意一下以下相關屬性:
/**
* FiberNode建構式
* @param tag 用于標記fiber節點的型別
* @param pendingProps 表示待處理的props資料
* @param key 用于唯一標識一個fiber節點(特別在一些串列資料結構中,一般會要求為每個DOM節點或組件加上額外的key屬性,在后續的調和階段會派上用場)
* @param mode 表示fiber節點的模式
* @constructor
*/
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
// 用于標記fiber節點的型別
this.tag = tag;
// 用于唯一標識一個fiber節點
this.key = key;
...
// 對于rootFiber節點而言,stateNode屬性指向對應的fiberRoot節點
// 對于child fiber節點而言,stateNode屬性指向對應的組件實體
this.stateNode = null;
// Fiber
// 以下屬性創建單鏈表樹結構
// return屬性始終指向父節點
// child屬性始終指向第一個子節點
// sibling屬性始終指向第一個兄弟節點
this.return = null;
this.child = null;
this.sibling = null;
// index屬性表示當前fiber節點的索引
this.index = 0;
...
// 表示待處理的props資料
this.pendingProps = pendingProps;
// 表示之前已經存盤的props資料
this.memoizedProps = null;
// 表示更新佇列
// 例如在常見的setState操作中
// 其實會先將需要更新的資料存放到這里的updateQueue佇列中用于后續調度
this.updateQueue = null;
// 表示之前已經存盤的state資料
this.memoizedState = null;
...
// 表示fiber節點的模式
this.mode = mode;
// 表示當前更新任務的過期時間,即在該時間之后更新任務將會被完成
this.expirationTime = NoWork;
// 表示當前fiber節點的子fiber節點中具有最高優先級的任務的過期時間
// 該屬性的值會根據子fiber節點中的任務優先級進行動態調整
this.childExpirationTime = NoWork;
// 用于指向另一個fiber節點
// 這兩個fiber節點使用alternate屬性相互參考,形成雙緩沖
// alternate屬性指向的fiber節點在任務調度中又稱為workInProgress節點
this.alternate = null;
...
}
其他有用的屬性筆者已經在原始碼中寫好相關注釋,感興趣的朋友可以在Github上查看完整的注釋資訊幫助理解,當然在現階段,其中的一些屬性還暫時難以理解,不過沒有關系,在后續的內容和系列文章中將會逐個擊破,在本小節中我們主要是為了理解FiberRoot和RootFiber這兩個容易混淆的概念以及兩者之間的聯系,同時在這里我們需要特別注意的是,多個fiber節點可形成基于單鏈表的樹形結構,通過自身的return,child和sibling屬性可以在多個fiber節點之間建立聯系,為了更加容易理解多個fiber節點及其屬性之間的關系,這里先回顧一下在上一篇文章中的簡單示例,我們在src/App.js檔案中將create-react-app腳手架生成的默認根組件App修改為如下形式:
import React, {Component} from 'react';
function List({data}) {
return (
<ul className="data-list">
{
data.map(item => {
return <li className="data-item" key={item}>{item}</li>
})
}
</ul>
);
}
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [1, 2, 3]
};
}
render() {
return (
<div className="container">
<h1 className="title">React learning</h1>
<List data=https://www.cnblogs.com/tangshiwei/p/{this.state.data} />
