React 是如何創(chuàng)建 vdom 和 fiber tree

-
作者:linxiangjun -
原文鏈接:https://www.linxiangjun.com/react-render-source.html#JSX
前言
本篇文章作為react源碼分析與優(yōu)化寫作計(jì)劃的第一篇,分析了react是如何創(chuàng)建vdom和fiber tree的。本篇文章通過閱讀react 16.8及以上版本源碼以及參考大量分析文章寫作而成,react框架本身算法以及架構(gòu)層也是不斷的在優(yōu)化,所以源碼中存在很多legacy的方法,不過這并不影響我們對于react設(shè)計(jì)思想的學(xué)習(xí)和理解。
閱讀源碼一定要帶著目的性的去展開,這樣就會減少過程中的枯燥感,而寫作能夠提煉和升華自己的學(xué)習(xí)和理解,這也是本篇以及后續(xù)文章的動力所在。如果寫作的文章還能夠幫助到其他開發(fā)者,那就更好了。
JSX
首先,來看一個(gè)簡單的 React 組件。
import React from 'react';
export default function App() {
return (
<div className="App">
<h1>Hello Reacth1>
div>
);
}
上面常用的語法稱之為 JSX,是 React.createElement 方法的語法糖,使用 JSX 能夠直觀的展現(xiàn) UI 及其交互,實(shí)現(xiàn)關(guān)注點(diǎn)分離。
每個(gè) react 組件的頂部都要導(dǎo)入 React,因?yàn)?JSX 實(shí)際上依賴 Babel(@babel/preset-react)來對語法進(jìn)行轉(zhuǎn)換,最終生成React.createElemnt的嵌套語法。
下方能夠直觀的看到 JSX 轉(zhuǎn)換后的渲染結(jié)果。
function App() {
return React.createElement(
'div',
{
className: 'App',
},
React.createElement('h1', null, 'Hello React')
);
}
createElement
createElement()方法定義如下:
React.createElement(type, [props], [...children]);
createElement()接收三個(gè)參數(shù),分別是元素類型、屬性值以及子元素,它最終會生成 Virtual DOM。
我們將上面的
組件內(nèi)容打印到控制臺中。
可以看到 Virtual DOM 本質(zhì)上是 JS 對象,將節(jié)點(diǎn)信息通過鍵值對的方式存儲起來,同時(shí)使用嵌套來表示節(jié)點(diǎn)間的層級關(guān)系。使用 VDOM 能夠避免頻繁的進(jìn)行 DOM 操作,同時(shí)也為后面的 React Diff 算法創(chuàng)造了條件。現(xiàn)在回到createElement()方法,來看一下它究竟是如何生產(chǎn) VDOM 的。
createElement()方法精簡版(v16.8)

首先,createElement()方法會先通過遍歷config獲取所有的參數(shù),然后獲取其子節(jié)點(diǎn)以及默認(rèn)的props的值。然后將值傳遞給ReactElement()調(diào)用并返回 JS 對象。

值得注意的是,每個(gè) react 組件都會使用$$typeof來標(biāo)識,它的值使用了Symbol數(shù)據(jù)結(jié)構(gòu)來確保唯一性。
ReactDOM.render
到目前為止,我們得到了 VDOM,react通過協(xié)調(diào)算法(reconciliation)去比較更新前后的VDOM,從而找到需要更新的最小操作,減少了瀏覽器多次操作DOM的成本。但是,由于使用遞歸的方式來遍歷組件樹,當(dāng)組件樹越來越大,遞歸遍歷的成本就越高。這樣,由于持續(xù)占用主線程,像布局、動畫等任務(wù)無法立即得到處理,就會出現(xiàn)丟幀的現(xiàn)象。所以,為不同類型的任務(wù)賦予優(yōu)先級,同時(shí)支持任務(wù)的暫停、中止與恢復(fù),是非常有必要的。
為了解決上面存在的問題,React團(tuán)隊(duì)給出了React Fiber算法以及fiber tree數(shù)據(jù)結(jié)構(gòu)(基于單鏈表的樹結(jié)構(gòu)),而ReactDOM.render方法就是實(shí)現(xiàn)React Fiber算法以及構(gòu)建fiber tree的核心API。
render()方法定義如下:
ReactDOM.render(element, container[, callback])
這里重點(diǎn)從源碼層面講解下ReactDOM.render是如何構(gòu)建fiber tree的。
ReactDOM.render實(shí)際調(diào)用了legacyRenderSubtreeIntoContainer方法,調(diào)用過程以及傳參如下:
ReactDOM = {
render(element, container, callback) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback
);
},
};
其中的element和container我們都很熟悉了,而callback是用來渲染完成后需要執(zhí)行的回調(diào)函數(shù)。再來看看該方法的定義。
function legacyRenderSubtreeIntoContainer(
parentComponent,
children,
container,
forceHydrate,
callback
) {
let root = container._reactRootContainer;
let fiberRoot;
// 初次渲染
if (!root) {
// 初始化掛載,獲得React根容器對象
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate
);
fiberRoot = root._internalRoot;
// 初始化安裝不需要批量更新,需要盡快完成
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
上面是簡化后的源碼。先來看傳參,因?yàn)槭菕燧droot,所以parentComponent設(shè)置為null。另外一個(gè)參數(shù)forceHydrate代表是否是服務(wù)端渲染,因?yàn)檎{(diào)用的render()方法為客服端渲染,所以默認(rèn)為false。另外callback使用少,所以關(guān)于它的處理過程就省略了。
因?yàn)槭鞘状螔燧d,所以root從container._reactRootContainer獲取不到值,就會創(chuàng)建FiberRoot對象。在FiberRoot對象創(chuàng)建過程中考慮到了服務(wù)端渲染的情況,并且函數(shù)之間相互調(diào)用非常多,所以這里直接展示其最終調(diào)用的核心方法。
// 創(chuàng)建fiberRoot和rootFiber并相互引用
function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
const root = new FiberRootNode(containerInfo, tag, hydrate);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// 創(chuàng)建fiber tree的根節(jié)點(diǎn),即rootFiber
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
}
在該方法中containerInfo就是root節(jié)點(diǎn),而tag為FiberRoot節(jié)點(diǎn)的標(biāo)記,這里為LegacyRoot。另外兩個(gè)參數(shù)和服務(wù)端渲染有關(guān)。這里使用FiberRootNode方法創(chuàng)建了FiberRoot對象,并使用createHostRootFiber方法創(chuàng)建RootFiber對象,使FiberRoot中的current指向RootFiber,RootFiber的stateNode指向FiberRoot,形成相互引用。
下面的兩個(gè)構(gòu)造函數(shù)是展現(xiàn)出了fiberRoot以及rootFiber的部分重要的屬性。
FiberRootNode部分屬性:
function FiberRootNode(containerInfo, tag, hydrate) {
// 用于標(biāo)記fiberRoot的類型
this.tag = tag;
// 指向當(dāng)前激活的與之對應(yīng)的rootFiber節(jié)點(diǎn)
this.current = null;
// 和fiberRoot關(guān)聯(lián)的DOM容器的相關(guān)信息
this.containerInfo = containerInfo;
// 當(dāng)前的fiberRoot是否處于hydrate模式
this.hydrate = hydrate;
// 每個(gè)fiberRoot實(shí)例上都只會維護(hù)一個(gè)任務(wù),該任務(wù)保存在callbackNode屬性中
this.callbackNode = null;
// 當(dāng)前任務(wù)的優(yōu)先級
this.callbackPriority = NoPriority;
}
Fiber Node構(gòu)造函數(shù)的部分屬性:
function FiberNode(tag, pendingProps, key, mode) {
// rootFiber指向fiberRoot,child fiber指向?qū)?yīng)的組件實(shí)例
this.stateNode = null;
// return屬性始終指向父節(jié)點(diǎn)
this.return = null;
// child屬性始終指向第一個(gè)子節(jié)點(diǎn)
this.child = null;
// sibling屬性始終指向第一個(gè)兄弟節(jié)點(diǎn)
this.sibling = null;
// 表示更新隊(duì)列,例如在常見的setState操作中,會將需要更新的數(shù)據(jù)存放到updateQueue隊(duì)列中用于后續(xù)調(diào)度
this.updateQueue = null;
// 表示當(dāng)前更新任務(wù)的過期時(shí)間,即在該時(shí)間之后更新任務(wù)將會被完成
this.expirationTime = NoWork;
}
最終生成的fiber tree結(jié)構(gòu)示意圖如下:

React Diff 算法
react 并不會比原生操作 DOM 快,但是在大型應(yīng)用中,往往不需要每次全部重新渲染,這時(shí) react 通過 VDOM 以及 diff 算法能夠只更新必要的 DOM。react 將 VDOM 與 diff 算法結(jié)合起來并對其進(jìn)行優(yōu)化,提供了高性能的 React Diff 算法,通過一系列的策略,將傳統(tǒng)的 diff 算法復(fù)雜度 O(n^3)優(yōu)化為 O(n)的復(fù)雜度,極大的提升了渲染性能。
這里不展開探究 React Diff 的具體實(shí)現(xiàn)原理,而先了解下它到底的基于什么策略來實(shí)現(xiàn)的。
-
Web UI 中 DOM 節(jié)點(diǎn)跨層級的移動操作特別少,可以忽略不計(jì)。 -
擁有相同類的兩個(gè)組件將會生成相似的樹形結(jié)構(gòu),擁有不同類的兩個(gè)組件將會生成不同的樹形結(jié)構(gòu)。 -
對于同一層級的一組子節(jié)點(diǎn),它們可以通過唯一 id 進(jìn)行區(qū)分。
基于這三個(gè)策略,react 在 tree diff 和 component diff 中,兩棵樹只會對同層次的節(jié)點(diǎn)進(jìn)行比較。如果同層級的樹發(fā)生了更新,則會將該節(jié)點(diǎn)及其子節(jié)點(diǎn)同時(shí)進(jìn)行更新,這樣避免了遞歸遍歷更加深入的節(jié)點(diǎn)的操作。在后面渲染性能優(yōu)化部分,對于同一類型的組件如果能夠準(zhǔn)確的知道 VDOM 是否變化,使用shouldComponentUpdate來判斷該組件是否需要 diff,能夠節(jié)省大量的 diff 運(yùn)算時(shí)間。
當(dāng) react 進(jìn)行 element diff 操作中,在元素中添加唯一的key來進(jìn)行區(qū)分,對其進(jìn)行算法優(yōu)化。所以像大數(shù)據(jù)量的列表之類的組件中最好添加key屬性,能夠帶來一定的性能提升。
交流討論
歡迎關(guān)注公眾號「前端試煉」,公眾號平時(shí)會分享一些實(shí)用或者有意思的東西,發(fā)現(xiàn)代碼之美。專注深度和最佳實(shí)踐,希望打造一個(gè)高質(zhì)量的公眾號。
公眾號后臺回復(fù)「加群」,拉你進(jìn)交流劃水聊天群,有看到好文章/代碼都會發(fā)在群里。
如果你不想加群,只是想加我也是可以。
如果覺得這篇文章還不錯(cuò),來個(gè)【轉(zhuǎn)發(fā)、收藏、在看】三連吧,讓更多的人也看到~
?? 順手點(diǎn)個(gè)在看唄 ↓?
