新聞中心
引言
相信大家對(duì) React 都已經(jīng)非常熟悉了,像 React,Vue 這樣的現(xiàn)代前端框架已經(jīng)是我們?nèi)粘i_(kāi)發(fā)離不開(kāi)的工具了,這篇文章主要是從源碼的角度剖析 React 的核心渲染原理。我們將從用戶編寫的組件代碼開(kāi)始,一步一步分析 React 是如何將它們變成真實(shí) DOM ,這個(gè)過(guò)程主要可以分成兩個(gè)階段:render 階段和 commit 階段。文章的核心內(nèi)容也正是對(duì)這兩個(gè)階段的分析。

專業(yè)從事成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè),高端網(wǎng)站制作設(shè)計(jì),小程序開(kāi)發(fā),網(wǎng)站推廣的成都做網(wǎng)站的公司。優(yōu)秀技術(shù)團(tuán)隊(duì)竭力真誠(chéng)服務(wù),采用HTML5+CSS3前端渲染技術(shù),自適應(yīng)網(wǎng)站建設(shè),讓網(wǎng)站在手機(jī)、平板、PC、微信下都能呈現(xiàn)。建站過(guò)程建立專項(xiàng)小組,與您實(shí)時(shí)在線互動(dòng),隨時(shí)提供解決方案,暢聊想法和感受。
一、前置知識(shí)
聲明式渲染
『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機(jī)器去做,與之相對(duì)的是『命令式渲染』。
『命令式渲染』則是由用戶去一步一步地命令機(jī)器下一步該怎么做。
舉個(gè)簡(jiǎn)單的例子:
如果我們需要在網(wǎng)頁(yè)上渲染一個(gè)有三個(gè)節(jié)點(diǎn)的列表,命令式的做法是手動(dòng)操作 dom,首先創(chuàng)建一個(gè)容器節(jié)點(diǎn),再利用循環(huán)每次先創(chuàng)建一個(gè)新節(jié)點(diǎn),填充內(nèi)容,然后將新節(jié)點(diǎn)新增到容器節(jié)點(diǎn)下,最后再將容器節(jié)點(diǎn)新增到 body 標(biāo)簽下:
- const list = [1,2,3];
- const container = document.createElement('div');
- for (let i = 0; i < list.length; i ++) {
- const newDom = document.createElement('div');
- newDom.innerHTML = list[i];
- container.appendChild(newDom);
- }
- document.body.appendChild(container);
而聲明式的做法應(yīng)該是:
- const list = [1,2,3];
- const container = document.createElement('div');
- const Demo = () =>
- (
)- {list.map((item) =>
{item})}- ReactDom.render(
, container);
可以看到在這個(gè)例子中,聲明式寫法以 HTML 語(yǔ)法直接告訴機(jī)器,我需要的視圖應(yīng)該是長(zhǎng)這個(gè)樣子,然后具體的 DOM 操作全部交由機(jī)器去完成。開(kāi)發(fā)者只需要專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。
這便是聲明式渲染。
聲明式渲染是現(xiàn)代前端框架的比較普遍的設(shè)計(jì)思路。
JSX 和 ReactElement
相信大家最初學(xué) React 的時(shí)候都有這樣的疑問(wèn),為什么我們能夠以類似 HTML 的語(yǔ)法編寫組件,這個(gè)東西又是怎么轉(zhuǎn)換成 JavaScript 語(yǔ)法的?答案就是 Babel。根據(jù)官網(wǎng)介紹,這種語(yǔ)法被稱為 JSX,是一個(gè) JavaScript 的語(yǔ)法擴(kuò)展。能夠被 Babel 編譯成 React.createElement 方法。舉個(gè)例子:
通過(guò)查閱源碼我們可以看到 「React.createElement」 方法。
- export function createElement(type, config, children) {
- let propName;
- // Reserved names are extracted
- const props = {};
- let key = null;
- let ref = null;
- let self = null;
- let source = null;
- ...
- return ReactElement(
- type,
- key,
- ref,
- self,
- source,
- ReactCurrentOwner.current,
- props,
- );
- }
- const ReactElement = function(type, key, ref, self, source, owner, props) {
- const element = {
- // This tag allows us to uniquely identify this as a React Element
- $typeof: REACT_ELEMENT_TYPE,
- // Built-in properties that belong on the element
- type: type,
- key: key,
- ref: ref,
- props: props,
- // Record the component responsible for creating this element.
- _owner: owner,
- };
- ...
- return element;
- }
可以看到 React 是使用了 element 這種結(jié)構(gòu)來(lái)代表一個(gè)節(jié)點(diǎn),里面就只有簡(jiǎn)單的 6 個(gè)字段。我們可以看個(gè)實(shí)際的例子,下面 Count 組件對(duì)應(yīng)的 element 數(shù)據(jù)結(jié)構(gòu):
- function Count({count, onCountClick}) {
- return
{ onCountClick()}}>- count: {count}
- }
可以看到,element 結(jié)構(gòu)只能反映出 jsx 節(jié)點(diǎn)的層級(jí)結(jié)構(gòu),而組件里的各種狀態(tài)或者返回 jsx 等都是不會(huì)記錄在 element 中。
目前我們知道,我們編寫的 jsx 會(huì)首先被處理成 element 結(jié)構(gòu)。
jsx -> element
那 React 又是如何處理 element 的,如剛剛說(shuō)的,element 里包含的信息太少,只靠 element 顯然是不足以映射到所有真實(shí) DOM 的,因此我們還需要更精細(xì)的結(jié)構(gòu)。
Fiber 樹(shù)結(jié)構(gòu)
Fiber 這個(gè)單詞相信大家多多少少都有聽(tīng)過(guò),它是在 React 16 被引入,關(guān)于 Fiber 如何實(shí)現(xiàn)任務(wù)調(diào)度在這篇文章不會(huì)涉及,但是 Fiber 的引入不僅僅帶來(lái)了任務(wù)調(diào)度方面的能力,整個(gè) React 實(shí)現(xiàn)架構(gòu)也因此重構(gòu)了一遍,而我們之前經(jīng)常提到的虛擬 DOM 樹(shù)在新的 React 架構(gòu)下被稱為 Fiber 樹(shù),上面提到的每個(gè) element 都有一個(gè)所屬的 Fiber。
首先我們先看看源碼中 Fiber 的構(gòu)造函數(shù):
- function FiberNode(
- tag: WorkTag,
- pendingProps: mixed,
- key: null | string,
- mode: TypeOfMode,
- ) {
- // Instance
- this.tag = tag; // 標(biāo)識(shí)節(jié)點(diǎn)類型,例如函數(shù)組件、類組件、普通標(biāo)簽等
- this.key = key;
- this.elementType = null; // 標(biāo)識(shí)具體 jsx 標(biāo)簽名
- this.type = null; // 類似 elementType
- this.stateNode = null; // 對(duì)應(yīng)的真實(shí) DOM 節(jié)點(diǎn)
- // Fiber
- this.return = null; // 父節(jié)點(diǎn)
- this.child = null; // 第一個(gè)子節(jié)點(diǎn)
- this.sibling = null; // 第一個(gè)兄弟節(jié)點(diǎn)
- this.index = 0;
- this.ref = null;
- this.pendingProps = pendingProps; // 傳入的 props
- this.memoizedProps = null;
- this.updateQueue = null; // 狀態(tài)更新相關(guān)
- this.memoizedState = null;
- this.dependencies = null;
- this.mode = mode;
- // Effects
- this.flags = NoFlags;
- this.subtreeFlags = NoFlags;
- this.deletions = null;
- this.lanes = NoLanes;
- this.childLanes = NoLanes;
- this.alternate = null;
- ...
- }
可以看到 Fiber 節(jié)點(diǎn)中的屬性很多,其中不僅僅包含了 element 相關(guān)的實(shí)例信息,還包含了組成 Fiber 樹(shù)所需的一些“指針”,組件內(nèi)部的狀態(tài)(memorizedState),用于操作真實(shí) DOM 的副作用(effects)等等。
我們以上面的 Count 組件為例看一下它對(duì)應(yīng)的 Fiber 結(jié)構(gòu):
這里我們先主要介紹一下與形成 Fiber 樹(shù)相關(guān)的三個(gè)屬性:child, sibling 和 return。他們分別指向 Fiber 的第一個(gè)子 Fiber,下一個(gè)兄弟 Fiber 和父 Fiber。
以下面的 jsx 代碼為例:
- // App.jsx
- text
- // Count.jsx
最終形成的 Fiber 樹(shù)結(jié)構(gòu)為:
總結(jié)一下,我們編寫的 jsx 首先會(huì)形成 element ,然后在 render 過(guò)程中每個(gè) element 都會(huì)生成對(duì)應(yīng)的 Fiber,最終形成 Fiber 樹(shù)。
jsx -> element -> Fiber
下面我們正式介紹一下 render 的過(guò)程,看看 Fiber 是如何生成并形成 Fiber 樹(shù)的。
二、渲染(render)過(guò)程
核心流程
通常 React 運(yùn)行時(shí)會(huì)有兩個(gè) Fiber 樹(shù),一個(gè)是根據(jù)當(dāng)前最新組件狀態(tài)構(gòu)建出來(lái)的,另一個(gè)則是上一次構(gòu)建出來(lái)的 Fiber 樹(shù),當(dāng)然如果是首次渲染就沒(méi)有上一次的 Fiber 樹(shù),這時(shí)就只有一個(gè)了。簡(jiǎn)單來(lái)說(shuō),render 過(guò)程就是 React 「對(duì)比舊 Fiber 樹(shù)和新的 element」 然后「為新的 element 生成新 Fiber 樹(shù)」的一個(gè)過(guò)程。
從源碼中看,React 的整個(gè)核心流程開(kāi)始于 「performSyncWorkOnRoot」 函數(shù),在這個(gè)函數(shù)里會(huì)先后調(diào)用 「renderRootSync」 函數(shù)和 「commitRoot」 函數(shù),它們兩個(gè)就是分別就是我們上面提到的 render 和 commit 過(guò)程。來(lái)看 renderRootSync 函數(shù),在 「renderRootSync」 函數(shù)里會(huì)先調(diào)用 「prepareFreshStack」 ,從函數(shù)名字我們不難猜出它主要就是為接下來(lái)的工作做前置準(zhǔn)備,初始化一些變量例如 workInProgress(當(dāng)前正在處理的 Fiber 節(jié)點(diǎn)) 等,接著會(huì)調(diào)用 「workLoopSync」 函數(shù)。(這里僅討論傳統(tǒng)模式,concurrent 模式留給 Fiber 任務(wù)調(diào)度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下來(lái)就會(huì)調(diào)用 commitRoot 進(jìn)入 commit 階段。
因此整個(gè) render 過(guò)程的重點(diǎn)在 「workLoopSync」 中,從 「workLoopSync」 簡(jiǎn)單的函數(shù)定義里我們可以看到,這里用了一個(gè)循環(huán)來(lái)不斷調(diào)用 「performUnitOfWork」 方法,直到 workInProgress 為 null。
- function workLoopSync() {
- // Already timed out, so perform work without checking if we need to yield.
- while (workInProgress !== null) {
- performUnitOfWork(workInProgress);
- }
- }
而 「performUnitOfWork」 函數(shù)做的事情也很簡(jiǎn)單,簡(jiǎn)單來(lái)說(shuō)就是為傳進(jìn)來(lái)的 workInProgress 生成下一個(gè) Fiber 節(jié)點(diǎn)然后賦值給 workInProgress。通過(guò)不斷的循環(huán)調(diào)用 「performUnitOfWork」,直到把所有的 Fiber 都生成出來(lái)并連接成 Fiber 樹(shù)為止。
現(xiàn)在我們來(lái)看 「performUnitOfWork」 具體是如何生成 Fiber 節(jié)點(diǎn)的。
前面介紹 Fiber 結(jié)構(gòu)的時(shí)候說(shuō)過(guò),F(xiàn)iber 是 React 16 引入用于任務(wù)調(diào)度提升用戶體驗(yàn)的,而在此之前,render 過(guò)程是遞歸實(shí)現(xiàn)的,顯然遞歸是沒(méi)有辦法中斷的,因此 React 需要使用循環(huán)來(lái)模擬遞歸過(guò)程。
「performUnitOfWork」 正是使用了 「beginWork」 和 「completeUnitOfWork」 來(lái)分別模擬這個(gè)“遞”和“歸”的過(guò)程。
render 過(guò)程是深度優(yōu)先的遍歷,「beginWork」 函數(shù)則會(huì)為遍歷到的每個(gè) Fiber 節(jié)點(diǎn)生成他的所有子 Fiber 并返回第一個(gè)子 Fiber ,這個(gè)子 Fiber 將賦值給 workInProgress,在下一輪循環(huán)繼續(xù)處理,直到遍歷到葉子節(jié)點(diǎn),這時(shí)候就需要“歸”了。
「completeUnitOfWork」 就會(huì)為葉子節(jié)點(diǎn)做一些處理,然后把葉子節(jié)點(diǎn)的兄弟節(jié)點(diǎn)賦值給 workInProgress 繼續(xù)“遞”操作,如果連兄弟節(jié)點(diǎn)也沒(méi)有的話,就會(huì)往上處理父節(jié)點(diǎn)。
同樣以上面的 Fiber 樹(shù)例子來(lái)看,其中的 Fiber 節(jié)點(diǎn)處理順序應(yīng)該如下:
beginWork
在介紹概覽的時(shí)候說(shuō)過(guò),React 通常會(huì)同時(shí)存在兩個(gè) Fiber 樹(shù),一個(gè)是當(dāng)前視圖對(duì)應(yīng)的,一個(gè)則是根據(jù)最新?tīng)顟B(tài)正在構(gòu)建中的。這兩棵樹(shù)的節(jié)點(diǎn)一一對(duì)應(yīng),我們用 current 來(lái)代表前者,我們不難發(fā)現(xiàn),當(dāng)首次渲染的時(shí)候,current 必然指向 null。實(shí)際上在代碼中也確實(shí)都是通過(guò)這個(gè)來(lái)判斷當(dāng)前是首次渲染還是更新。
「beginWork」 的目的很簡(jiǎn)單:
- 更新當(dāng)前節(jié)點(diǎn)(workInProgress),獲取新的 children。
- 為新的 children 生成他們對(duì)應(yīng)的 Fiber,并「最終返回第一個(gè)子節(jié)點(diǎn)(child)」。
在 「beginWork」 執(zhí)行中,首先會(huì)判斷當(dāng)前是否是首次渲染。
- 如果是首次渲染:
- 則下來(lái)會(huì)根據(jù)當(dāng)前正在構(gòu)建的節(jié)點(diǎn)的組件類型做不同的處理,源碼中這塊邏輯使用了大量的 switch case。
- switch (workInProgress.tag) {
- case FunctionComponent: {
- ...
- }
- case ClassComponent: {
- ...
- }
- case HostRoot: {
- ...
- }
- case HostComponent: {
- ...
- }
- ...
- }
- 如果非首次渲染:
- React 會(huì)使用一些優(yōu)化手段,而符合優(yōu)化的條件則是「當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)組件的 props 和 context 沒(méi)有發(fā)生變化」并且**當(dāng)前節(jié)點(diǎn)的更新優(yōu)先級(jí)不夠,**如果這兩個(gè)條件均滿足的話可以直接復(fù)制 current 的子節(jié)點(diǎn)并返回。如果不滿足則同首次渲染走一樣的邏輯。
- if (current !== null) {
- // 這里處理一些依賴
- if (
- enableLazyContextPropagation &&
- !includesSomeLane(renderLanes, updateLanes)
- ) {
- const dependencies = current.dependencies;
- if (dependencies !== null && checkIfContextChanged(dependencies)) {
- updateLanes = mergeLanes(updateLanes, renderLanes);
- }
- }
- const oldProps = current.memoizedProps;
- const newProps = workInProgress.pendingProps;
- if (
- oldProps !== newProps ||
- hasLegacyContextChanged() ||
- // Force a re-render if the implementation changed due to hot reload:
- (__DEV__ ? workInProgress.type !== current.type : false)
- ) {
- // 如果 props 或者 context 變了
- didReceiveUpdate = true;
- } else if (!includesSomeLane(renderLanes, updateLanes)) {
- didReceiveUpdate = false;
- // 走到這里則說(shuō)明符合優(yōu)化條件
- switch (workInProgress.tag) {
- case HostRoot:
- ...
- break;
- case HostComponent:
- ...
- break;
- case ClassComponent: {
- ...
- break;
- }
- case HostPortal:
- ...
- break;
- case ContextProvider: {
- ...
- break;
- }
- ...
- }
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- } else {
- ...
- didReceiveUpdate = false;
- }
- } else {
- didReceiveUpdate = false;
- }
更新優(yōu)化策略應(yīng)用
開(kāi)發(fā)過(guò)程中我們常常希望利用 React 非首次渲染的優(yōu)化策略來(lái)提升性能,如下代碼,B 組件是個(gè)純展示組件且內(nèi)部沒(méi)有依賴任何 Demo 組件的數(shù)據(jù),因此有些同學(xué)可能會(huì)想當(dāng)然認(rèn)為當(dāng) Demo 重新渲染時(shí)這個(gè) B 組件是符合 React 優(yōu)化條件的。但結(jié)果是,每次 Demo 重新渲染都會(huì)導(dǎo)致 B 組件重新渲染。每次渲染時(shí) B 組件的 props 看似沒(méi)發(fā)生變化,但由于 Demo 重新執(zhí)行后會(huì)生成全新的 B 組件(下面會(huì)介紹),所以新舊 B 組件的 props 肯定也是不同的。
- function App() {
- return
- }
- function Demo() {
- const [v, setV] = useState();
- return (
- );
- }
那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來(lái),B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因?yàn)椴还?Demo 組件狀態(tài)怎么變化都不會(huì)影響到 App 組件,因此 App 和 B 組件就只會(huì)在首次渲染時(shí)會(huì)執(zhí)行一遍,也就是說(shuō) Demo 獲取到的 props.children 的引用一直都是指向同一個(gè)對(duì)象,這樣一來(lái) B 組件的 props 也就不會(huì)變化了。
- function App() {
- return
- }
- function Demo(props) {
- const [v, setV] = useState();
- return (
- );
- }
更新當(dāng)前節(jié)點(diǎn)
通過(guò)上面的解析我們知道,當(dāng)不走優(yōu)化邏輯時(shí) 「beginWork」 使用大量的 switch...case 來(lái)分別處理不同類型的組件,下來(lái)我們以我們熟悉的 Function Component 為例。
「核心就是通過(guò)調(diào)用函數(shù)組件,得到組件的返回的 element?!?/p>
類似地,對(duì)于類組件,則是調(diào)用組件實(shí)例的 render 方法得到 element。
而對(duì)于我們普通的組件,例如
,則是直接取 props.children 即可。
- function updateFunctionComponent(
- current,
- workInProgress,
- Component,
- nextProps: any,
- renderLanes,
- ) {
- let context;
- if (!disableLegacyContext) {
- const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
- context = getMaskedContext(workInProgress, unmaskedContext);
- }
- let nextChildren;
- prepareToReadContext(workInProgress, renderLanes);
- // 執(zhí)行組件函數(shù)獲取返回的 element
- nextChildren = renderWithHooks(
- current,
- workInProgress,
- Component,
- nextProps,
- context,
- renderLanes,
- );
- // React DevTools reads this flag.
- workInProgress.flags |= PerformedWork;
- reconcileChildren(current, workInProgress, nextChildren, renderLanes);
- return workInProgress.child;
- }
得到組件返回的 element(s) 之后,下一步就是為他們生成 Fiber,我們查看源碼可以看到,不論是函數(shù)組件或是類組件或是普通組件,最后返回的 element(s) 都會(huì)作為參數(shù)傳入到 「reconcileChildren」 中。
介紹 「reconcileChildren」 之前我們先用一張圖總結(jié)一下 「beginWork」 的大致流程:
生成子節(jié)點(diǎn)
經(jīng)過(guò)上一步得到 workInProgress 的 children 之后,接下來(lái)需要為這些 children element 生成 Fiber ,這就是 「reconcileChildFibers」 函數(shù)做的事情,這也是我們經(jīng)常提到的 diff 的過(guò)程。
這個(gè)函數(shù)里主要分兩種情況處理,如果是 newChild(即 children element)是 object 類型,則進(jìn)入單節(jié)點(diǎn) diff 過(guò)程(「reconcileSingleElement」),如果是數(shù)組類型,則進(jìn)入多節(jié)點(diǎn) diff 過(guò)程(「reconcileChildrenArray」)。
- function reconcileChildFibers(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- newChild: any,
- lanes: Lanes,
- ): Fiber | null {
- if (typeof newChild === 'object' && newChild !== null) {
- switch (newChild.$typeof) {
- case REACT_ELEMENT_TYPE:
- return placeSingleChild(
- reconcileSingleElement(
- returnFiber,
- currentFirstChild,
- newChild,
- lanes,
- ),
- );
- ...
- }
- if (isArray(newChild)) {
- return reconcileChildrenArray(
- returnFiber,
- currentFirstChild,
- newChild,
- lanes,
- );
- }
- throwOnInvalidObjectType(returnFiber, newChild);
- }
- }
單節(jié)點(diǎn)diff
- function reconcileSingleElement(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- element: ReactElement,
- lanes: Lanes,
- ): Fiber {
- const key = element.key;
- let child = currentFirstChild;
- while (child !== null) {
- // 首先比較 key 是否相同
- if (child.key === key) {
- const elementType = element.type;
- ...
- // 然后比較 elementType 是否相同
- if (child.elementType === elementType) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props);
- existing.ref = coerceRef(returnFiber, child, element);
- existing.return = returnFiber;
- return existing;
- }
- // Didn't match.
- deleteRemainingChildren(returnFiber, child);
- break;
- } else {
- deleteChild(returnFiber, child);
- }
- // 遍歷兄弟節(jié)點(diǎn),看能不能找到 key 相同的節(jié)點(diǎn)
- child = child.sibling;
- }
- if (element.type === REACT_FRAGMENT_TYPE) {
- const created = createFiberFromFragment(
- element.props.children,
- returnFiber.mode,
- lanes,
- element.key,
- );
- created.return = returnFiber;
- return created;
- } else {
- const created = createFiberFromElement(element, returnFiber.mode, lanes);
- created.ref = coerceRef(returnFiber, currentFirstChild, element);
- created.return = returnFiber;
- return created;
- }
- }
本著盡可能復(fù)用舊節(jié)點(diǎn)的原則,在單節(jié)點(diǎn) diff 在這里,我們會(huì)遍歷舊節(jié)點(diǎn),對(duì)每個(gè)遍歷到的節(jié)點(diǎn)會(huì)做一下兩個(gè)判斷:
- key 是否相同
- key 相同的情況下,elementType 是否相同
延伸下來(lái)有三種情況:
- 如果 key 不相同,則直接調(diào)用 「deleteChild」 將這個(gè) child 標(biāo)記為刪除,但是我們不用灰心,可能只是我們還沒(méi)有找到那個(gè)對(duì)的節(jié)點(diǎn),所以要繼續(xù)執(zhí)行child = child.sibling;遍歷兄弟節(jié)點(diǎn),直到找到那個(gè)對(duì)的節(jié)點(diǎn)。
- 如果 key 相同,elementType 相同,那就是最理想的情況,找到了可以復(fù)用的節(jié)點(diǎn),直接調(diào)用 「deleteRemainingChildren」 把剩余的兄弟節(jié)點(diǎn)標(biāo)記刪除,然后直接復(fù)用 child 返回。
- 如果 key 相同,但 elementType 不同,這是最悲情的情況,我們找到了那個(gè)節(jié)點(diǎn),可惜的是這個(gè)節(jié)點(diǎn)的 elementType 已經(jīng)變了,那我們也不需要再找了,把 child 及其所有兄弟節(jié)點(diǎn)標(biāo)記刪除,跳出循環(huán)。直接創(chuàng)建一個(gè)新的節(jié)點(diǎn)。
多節(jié)點(diǎn)diff
- function reconcileChildrenArray(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- newChildren: Array<*>,
- lanes: Lanes,
- ) {
- let resultingFirstChild: Fiber | null = null;
- let previousNewFiber: Fiber | null = null;
- let oldFiber = currentFirstChild;
- let lastPlacedIndex = 0;
- let newIdx = 0;
- let nextOldFiber = null;
- for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
- const newFiber = updateSlot(
- returnFiber,
- oldFiber,
- newChildren[newIdx],
- lanes,
- );
- if (newFiber === null) {
- break;
- }
- lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
- if (previousNewFiber === null) {
- resultingFirstChild = newFiber;
- } else {
- previousNewFiber.sibling = newFiber;
- }
- previousNewFiber = newFiber;
- oldFiber = nextOldFiber;
- }
- if (newIdx === newChildren.length) {
- ...
- }
- if (oldFiber === null) {
- ...
- }
- for (; newIdx < newChildren.length; newIdx++) {
- ...
- }
- return resultingFirstChild;
- }
- function updateSlot(
- returnFiber: Fiber,
- oldFiber: Fiber | null,
- newChild: any,
- lanes: Lanes,
- ): Fiber | null {
- const key = oldFiber !== null ? oldFiber.key : null;
- ...
- if (newChild.key === key) {
- return updateElement(returnFiber, oldFiber, newChild, lanes);
- } else {
- return null;
- }
- }
從源碼我們可以看到,在 「reconcileChildrenArray」 中,出現(xiàn)了兩個(gè)循環(huán)。
第一輪循環(huán)中邏輯如下:
- 同時(shí)遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。
- 如果 key 相同。
- 判斷雙方 elementType 是否相同。
- 如果相同則復(fù)用 oldFiber 返回。
- 如果不同則新建 Fiber 返回。
- 如果 key 不同則直接跳出循環(huán)。
可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時(shí)就會(huì)跳出循環(huán),換句話說(shuō),第一輪循環(huán)里做的事情都是基于 key 相同,主要就是「更新」的工作。
跳出循環(huán)后,要先執(zhí)行兩個(gè)判斷:
- newChildren 已經(jīng)遍歷完了:這種情況說(shuō)明新的 children 全都已經(jīng)處理完了,只要把 oldFiber 和他所有剩余的兄弟節(jié)點(diǎn)刪除然后返回頭部的 Fiber 即可。
- 已經(jīng)沒(méi)有 oldFiber :這種情況說(shuō)明 children 有新增的節(jié)點(diǎn),給這些新增的節(jié)點(diǎn)逐一構(gòu)建 Fiber 并鏈接上,然后返回頭部的 Fiber 即可。
如果以上兩種情況都不是,則進(jìn)入第二輪循環(huán)。
在執(zhí)行第二輪循環(huán)之前,先把剩下的舊節(jié)點(diǎn)和他們對(duì)應(yīng)的 key 或者 index 做成映射,方便查找。
第二輪循環(huán)沿用了第一輪循環(huán)的 newIdx 變量,說(shuō)明第二輪循環(huán)是在第一輪循環(huán)結(jié)束的地方開(kāi)始再次遍歷剩下的 newChildren。
- const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
- for (; newIdx < newChildren.length; newIdx++) {
- const newFiber = updateFromMap(
- existingChildren,
- returnFiber,
- newIdx,
- newChildren[newIdx],
- lanes,
- );
- if (newFiber !== null) {
- if (shouldTrackSideEffects) {
- if (newFiber.alternate !== null) {
- existingChildren.delete(
- newFiber.key === null ? newIdx : newFiber.key,
- );
- }
- }
- lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
- if (previousNewFiber === null) {
- resultingFirstChild = newFiber;
- } else {
- previousNewFiber.sibling = newFiber;
- }
- previousNewFiber = newFiber;
- }
- }
- function placeChild(
- newFiber: Fiber,
- lastPlacedIndex: number,
- newIndex: number,
- ): number {
- newFiber.index = newIndex;
- if (!shouldTrackSideEffects) {
- // Noop.
- return lastPlacedIndex;
- }
- const current = newFiber.alternate;
- if (current !== null) {
- const oldIndex = current.index;
- if (oldIndex < lastPlacedIndex) {
- // This is a move.
- newFiber.flags |= Placement;
- return lastPlacedIndex;
- } else {
- // This item can stay in place.
- return oldIndex;
- }
- } else {
- // This is an insertion.
- newFiber.flags |= Placement;
- return lastPlacedIndex;
- }
- }
第二輪循環(huán)主要調(diào)用了 「updateFromMap」 來(lái)處理節(jié)點(diǎn),在這里需要用 newChild 的 key 去 existingChildren 中找對(duì)應(yīng)的 Fiber。
- 能找到 key 相同的,則說(shuō)明這個(gè)節(jié)點(diǎn)只是位置變了,是可以復(fù)用的。
- 找不到 key 相同的,則說(shuō)明這個(gè)節(jié)點(diǎn)應(yīng)該是新增的。
不管是復(fù)用還是新增,「updateFromMap」 都會(huì)返回一個(gè) newFiber,然后我們需要為這個(gè) newFiber 更新一下它的位置(index),但是僅僅更新這個(gè) Fiber 的 index 還不夠,因?yàn)檫@個(gè) Fiber 有可能是復(fù)用的,如果是復(fù)用的就意味著它已經(jīng)有對(duì)應(yīng)的真實(shí) DOM 節(jié)點(diǎn)了,我們還需要復(fù)用它的真實(shí) DOM,因此需要對(duì)應(yīng)更新這個(gè) Fiber 的 flag,但是真的需要對(duì)每個(gè) Fiber 都去設(shè)置 flag 嗎,我們舉個(gè)例子:
- // 舊
- [, , ]
- // 新
- [, , ]
如果按照我們剛剛說(shuō)的做法,這里的 a, b, c 都會(huì)被打上 flag,這樣一來(lái),在 commit 階段,這三個(gè) DOM 都會(huì)被移動(dòng),可是我們知道,這里顯然只需要移動(dòng)一個(gè)節(jié)點(diǎn)即可,退一萬(wàn)步說(shuō)我們移動(dòng)兩個(gè)節(jié)點(diǎn)也比移動(dòng)所有節(jié)點(diǎn)要來(lái)的聰明。
其實(shí)在這個(gè)問(wèn)題上主要就是我們得區(qū)分一下到底哪個(gè)節(jié)點(diǎn)才是移動(dòng)了的,這就需要一個(gè)參照點(diǎn),我們要保證在參照點(diǎn)左邊都是已經(jīng)排好順序了的。而這個(gè)參照點(diǎn)就是 lastPlacedIndex。有了它,我們?cè)诒闅v newChildren 的時(shí)候可能會(huì)出現(xiàn)下面兩種情況:
- 生成(或復(fù)用)的 Fiber 對(duì)應(yīng)的老 index < lastPlacedIndex,這就說(shuō)明這個(gè) Fiber 的位置不對(duì),因?yàn)?lastPlacedIndex 左邊的應(yīng)該全是已經(jīng)遍歷過(guò)的 newChild 生成的 Fiber。因此這個(gè) Fiber 是需要被移動(dòng)的,打上 flag。
- 如果 Fiber 對(duì)應(yīng)的老 index >= lastPlacedIndex,那就說(shuō)明這個(gè) Fiber 的相對(duì)位置是 ok 的,可以不用移動(dòng),但是我們需要更新一下參照點(diǎn),把參照點(diǎn)更新成這個(gè) Fiber 對(duì)應(yīng)的老 index。
我們舉一個(gè)例子:
- // 舊
- [, , , ]
- // 新
- [, , , , ]
lastPlacedIndex 初始值為 0,
首先處理
分享文章:一文讀懂React組件渲染核心原理
網(wǎng)址分享:http://m.5511xx.com/article/dpjoepc.html


咨詢
建站咨詢
