圖文并茂,一文讀懂 React 組件渲染核心原理
這是我們團(tuán)隊(duì)楊勁松、楊杰強(qiáng)同學(xué)做的內(nèi)部分享,文章從聲明式渲染的基本原理開始,逐步深入講解 React 渲染與節(jié)點(diǎn)掛載的基本流程與源碼,適合初中階同學(xué)閱讀。PS:我們是字節(jié)游戲中臺前端團(tuán)隊(duì),日常學(xué)習(xí)氛圍濃厚,目前還有大量 HC,歡迎自薦。
引言
相信大家對 React 都已經(jīng)非常熟悉了,像 React,Vue 這樣的現(xiàn)代前端框架已經(jīng)是我們?nèi)粘i_發(fā)離不開的工具了,這篇文章主要是從源碼的角度剖析 React 的核心渲染原理。我們將從用戶編寫的組件代碼開始,一步一步分析 React 是如何將它們變成真實(shí) DOM ,這個過程主要可以分成兩個階段:render 階段和 commit 階段。文章的核心內(nèi)容也正是對這兩個階段的分析。
一、前置知識
聲明式渲染
『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機(jī)器去做,與之相對的是『命令式渲染』。 『命令式渲染』則是由用戶去一步一步地命令機(jī)器下一步該怎么做。
舉個簡單的例子:
如果我們需要在網(wǎng)頁上渲染一個有三個節(jié)點(diǎn)的列表,命令式的做法是手動操作 dom,首先創(chuàng)建一個容器節(jié)點(diǎn),再利用循環(huán)每次先創(chuàng)建一個新節(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?????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?=?()?=>
(<div>
????{list.map((item)?=>?<div>{item}div>)}
div>)
ReactDom.render(<Demo?/>,?container);
可以看到在這個例子中,聲明式寫法以 HTML 語法直接告訴機(jī)器,我需要的視圖應(yīng)該是長這個樣子,然后具體的 DOM 操作全部交由機(jī)器去完成。開發(fā)者只需要專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。
這便是聲明式渲染。
聲明式渲染是現(xiàn)代前端框架的比較普遍的設(shè)計思路。
JSX 和 ReactElement
相信大家最初學(xué) React 的時候都有這樣的疑問,為什么我們能夠以類似 HTML 的語法編寫組件,這個東西又是怎么轉(zhuǎn)換成 JavaScript 語法的?答案就是 Babel。根據(jù)官網(wǎng)介紹,這種語法被稱為 JSX,是一個 JavaScript 的語法擴(kuò)展。能夠被 Babel 編譯成 React.createElement 方法。舉個例子:

通過查閱源碼我們可以看到 「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)來代表一個節(jié)點(diǎn),里面就只有簡單的 6 個字段。我們可以看個實(shí)際的例子,下面 Count 組件對應(yīng)的 element 數(shù)據(jù)結(jié)構(gòu):
function?Count({count,?onCountClick})?{
??return?<div?onClick={()?=>?{?onCountClick()}}>
??count:?{count}
??div>
}

可以看到,element 結(jié)構(gòu)只能反映出 jsx 節(jié)點(diǎn)的層級結(jié)構(gòu),而組件里的各種狀態(tài)或者返回 jsx 等都是不會記錄在 element 中。
目前我們知道,我們編寫的 jsx 會首先被處理成 element 結(jié)構(gòu)。
jsx -> element
那 React 又是如何處理 element 的,如剛剛說的,element 里包含的信息太少,只靠 element 顯然是不足以映射到所有真實(shí) DOM 的,因此我們還需要更精細(xì)的結(jié)構(gòu)。
Fiber 樹結(jié)構(gòu)
Fiber 這個單詞相信大家多多少少都有聽過,它是在 React 16 被引入,關(guān)于 Fiber 如何實(shí)現(xiàn)任務(wù)調(diào)度在這篇文章不會涉及,但是 Fiber 的引入不僅僅帶來了任務(wù)調(diào)度方面的能力,整個 React 實(shí)現(xiàn)架構(gòu)也因此重構(gòu)了一遍,而我們之前經(jīng)常提到的虛擬 DOM 樹在新的 React 架構(gòu)下被稱為 Fiber 樹,上面提到的每個 element 都有一個所屬的 Fiber。
首先我們先看看源碼中 Fiber 的構(gòu)造函數(shù):
function?FiberNode(
??tag:?WorkTag,
??pendingProps:?mixed,
??key:?null?|?string,
??mode:?TypeOfMode,
)?{
??//?Instance
??this.tag?=?tag;????????????//?標(biāo)識節(jié)點(diǎn)類型,例如函數(shù)組件、類組件、普通標(biāo)簽等
??this.key?=?key;
??this.elementType?=?null;??//?標(biāo)識具體?jsx?標(biāo)簽名
??this.type?=?null;????????//?類似?elementType
??this.stateNode?=?null;??//?對應(yīng)的真實(shí)?DOM?節(jié)點(diǎn)
??//?Fiber
??this.return?=?null;????//?父節(jié)點(diǎn)
??this.child?=?null;?????//?第一個子節(jié)點(diǎn)
??this.sibling?=?null;???//?第一個兄弟節(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 樹所需的一些“指針”,組件內(nèi)部的狀態(tài)(memorizedState),用于操作真實(shí) DOM 的副作用(effects)等等。
我們以上面的 Count 組件為例看一下它對應(yīng)的 Fiber 結(jié)構(gòu):

這里我們先主要介紹一下與形成 Fiber 樹相關(guān)的三個屬性:child, sibling 和 return。他們分別指向 Fiber 的第一個子 Fiber,下一個兄弟 Fiber 和父 Fiber。
以下面的 jsx 代碼為例:
?????
//?App.jsx????
????<div>
??????<header>
????????<img?/>
????????<p>
??????????text
????????p>
????????<Count?count={count}?onCountClick={handleCLick}?/>
??????header>
????div>
????
//?Count.jsx
<div>
div>
最終形成的 Fiber 樹結(jié)構(gòu)為:

總結(jié)一下,我們編寫的 jsx 首先會形成 element ,然后在 render 過程中每個 element 都會生成對應(yīng)的 Fiber,最終形成 Fiber 樹。
jsx -> element -> Fiber
下面我們正式介紹一下 render 的過程,看看 Fiber 是如何生成并形成 Fiber 樹的。
二、渲染(render)過程
核心流程
通常 React 運(yùn)行時會有兩個 Fiber 樹,一個是根據(jù)當(dāng)前最新組件狀態(tài)構(gòu)建出來的,另一個則是上一次構(gòu)建出來的 Fiber 樹,當(dāng)然如果是首次渲染就沒有上一次的 Fiber 樹,這時就只有一個了。簡單來說,render 過程就是 React 「對比舊 Fiber 樹和新的 element」 然后「為新的 element 生成新 Fiber 樹」的一個過程。
從源碼中看,React 的整個核心流程開始于 「performSyncWorkOnRoot」 函數(shù),在這個函數(shù)里會先后調(diào)用 「renderRootSync」 函數(shù)和 「commitRoot」 函數(shù),它們兩個就是分別就是我們上面提到的 render 和 commit 過程。來看 renderRootSync 函數(shù),在 「renderRootSync」 函數(shù)里會先調(diào)用 「prepareFreshStack」 ,從函數(shù)名字我們不難猜出它主要就是為接下來的工作做前置準(zhǔn)備,初始化一些變量例如 workInProgress(當(dāng)前正在處理的 Fiber 節(jié)點(diǎn)) 等,接著會調(diào)用 「workLoopSync」 函數(shù)。(這里僅討論傳統(tǒng)模式,concurrent 模式留給 Fiber 任務(wù)調(diào)度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下來就會調(diào)用 commitRoot 進(jìn)入 commit 階段。

因此整個 render 過程的重點(diǎn)在 「workLoopSync」 中,從 「workLoopSync」 簡單的函數(shù)定義里我們可以看到,這里用了一個循環(huán)來不斷調(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ù)做的事情也很簡單,簡單來說就是為傳進(jìn)來的 workInProgress 生成下一個 Fiber 節(jié)點(diǎn)然后賦值給 workInProgress。通過不斷的循環(huán)調(diào)用 「performUnitOfWork」,直到把所有的 Fiber 都生成出來并連接成 Fiber 樹為止。
現(xiàn)在我們來看 「performUnitOfWork」 具體是如何生成 Fiber 節(jié)點(diǎn)的。
前面介紹 Fiber 結(jié)構(gòu)的時候說過,F(xiàn)iber 是 React 16 引入用于任務(wù)調(diào)度提升用戶體驗(yàn)的,而在此之前,render 過程是遞歸實(shí)現(xiàn)的,顯然遞歸是沒有辦法中斷的,因此 React 需要使用循環(huán)來模擬遞歸過程。
「performUnitOfWork」 正是使用了 「beginWork」 和 「completeUnitOfWork」 來分別模擬這個“遞”和“歸”的過程。
render 過程是深度優(yōu)先的遍歷,「beginWork」 函數(shù)則會為遍歷到的每個 Fiber 節(jié)點(diǎn)生成他的所有子 Fiber 并返回第一個子 Fiber ,這個子 Fiber 將賦值給 workInProgress,在下一輪循環(huán)繼續(xù)處理,直到遍歷到葉子節(jié)點(diǎn),這時候就需要“歸”了。
「completeUnitOfWork」 就會為葉子節(jié)點(diǎn)做一些處理,然后把葉子節(jié)點(diǎn)的兄弟節(jié)點(diǎn)賦值給 workInProgress 繼續(xù)“遞”操作,如果連兄弟節(jié)點(diǎn)也沒有的話,就會往上處理父節(jié)點(diǎn)。
同樣以上面的 Fiber 樹例子來看,其中的 Fiber 節(jié)點(diǎn)處理順序應(yīng)該如下:

beginWork
在介紹概覽的時候說過,React 通常會同時存在兩個 Fiber 樹,一個是當(dāng)前視圖對應(yīng)的,一個則是根據(jù)最新狀態(tài)正在構(gòu)建中的。這兩棵樹的節(jié)點(diǎn)一一對應(yīng),我們用 current 來代表前者,我們不難發(fā)現(xiàn),當(dāng)首次渲染的時候,current 必然指向 null。實(shí)際上在代碼中也確實(shí)都是通過這個來判斷當(dāng)前是首次渲染還是更新。
「beginWork」 的目的很簡單:
更新當(dāng)前節(jié)點(diǎn)(workInProgress),獲取新的 children。 為新的 children 生成他們對應(yīng)的 Fiber,并「最終返回第一個子節(jié)點(diǎn)(child)」。
在 「beginWork」 執(zhí)行中,首先會判斷當(dāng)前是否是首次渲染。
如果是首次渲染: 則下來會根據(jù)當(dāng)前正在構(gòu)建的節(jié)點(diǎn)的組件類型做不同的處理,源碼中這塊邏輯使用了大量的 switch case。
switch?(workInProgress.tag)?{
????case?FunctionComponent:?{
??????...
????}
????case?ClassComponent:?{
??????...
????}
????case?HostRoot:?{
??????...
????}
????case?HostComponent:?{
??????...
????}
????...
??}
如果非首次渲染: React 會使用一些優(yōu)化手段,而符合優(yōu)化的條件則是「當(dāng)前節(jié)點(diǎn)對應(yīng)組件的 props 和 context 沒有發(fā)生變化」并且**當(dāng)前節(jié)點(diǎn)的更新優(yōu)先級不夠,**如果這兩個條件均滿足的話可以直接復(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;
??????//?走到這里則說明符合優(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)用
開發(fā)過程中我們常常希望利用 React 非首次渲染的優(yōu)化策略來提升性能,如下代碼,B 組件是個純展示組件且內(nèi)部沒有依賴任何 Demo 組件的數(shù)據(jù),因此有些同學(xué)可能會想當(dāng)然認(rèn)為當(dāng) Demo 重新渲染時這個 B 組件是符合 React 優(yōu)化條件的。但結(jié)果是,每次 Demo 重新渲染都會導(dǎo)致 B 組件重新渲染。每次渲染時 B 組件的 props 看似沒發(fā)生變化,但由于 Demo 重新執(zhí)行后會生成全新的 B 組件(下面會介紹),所以新舊 B 組件的 props 肯定也是不同的。
function?App()?{
????return?<Demo?/>
}
function?Demo()?{
????const?[v,?setV]?=?useState();
????return?(
????????<div>
????????????<A?value={v}?/>
????????????<B?/>
????????div>??
????);
}
那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來,B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因?yàn)椴还?Demo 組件狀態(tài)怎么變化都不會影響到 App 組件,因此 App 和 B 組件就只會在首次渲染時會執(zhí)行一遍,也就是說 Demo 獲取到的 props.children 的引用一直都是指向同一個對象,這樣一來 B 組件的 props 也就不會變化了。
function?App()?{
????return?<Demo>
????????<B?/>
????Demo>
}
function?Demo(props)?{
????const?[v,?setV]?=?useState();
????return?(
????????<div>
????????????<A?value={v}?/>
???????????{props.children}
????????div>??
????);
}
更新當(dāng)前節(jié)點(diǎn)
通過上面的解析我們知道,當(dāng)不走優(yōu)化邏輯時 「beginWork」 使用大量的 switch...case 來分別處理不同類型的組件,下來我們以我們熟悉的 Function Component 為例。
「核心就是通過調(diào)用函數(shù)組件,得到組件的返回的 element。」
類似地,對于類組件,則是調(diào)用組件實(shí)例的 render 方法得到 element。
而對于我們普通的組件,例如
,則是直接取 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) 都會作為參數(shù)傳入到 「reconcileChildren」 中。
介紹 「reconcileChildren」 之前我們先用一張圖總結(jié)一下 「beginWork」 的大致流程:

生成子節(jié)點(diǎn)
經(jīng)過上一步得到 workInProgress 的 children 之后,接下來需要為這些 children element 生成 Fiber ,這就是 「reconcileChildFibers」 函數(shù)做的事情,這也是我們經(jīng)常提到的 diff 的過程。
這個函數(shù)里主要分兩種情況處理,如果是 newChild(即 children element)是 object 類型,則進(jìn)入單節(jié)點(diǎn) diff 過程(「reconcileSingleElement」),如果是數(shù)組類型,則進(jìn)入多節(jié)點(diǎn) diff 過程(「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 在這里,我們會遍歷舊節(jié)點(diǎn),對每個遍歷到的節(jié)點(diǎn)會做一下兩個判斷:
key 是否相同 key 相同的情況下,elementType 是否相同
延伸下來有三種情況:
如果 key 不相同,則直接調(diào)用 「deleteChild」 將這個 child 標(biāo)記為刪除,但是我們不用灰心,可能只是我們還沒有找到那個對的節(jié)點(diǎn),所以要繼續(xù)執(zhí)行 child = child.sibling;遍歷兄弟節(jié)點(diǎn),直到找到那個對的節(jié)點(diǎn)。如果 key 相同,elementType 相同,那就是最理想的情況,找到了可以復(fù)用的節(jié)點(diǎn),直接調(diào)用 「deleteRemainingChildren」 把剩余的兄弟節(jié)點(diǎn)標(biāo)記刪除,然后直接復(fù)用 child 返回。 如果 key 相同,但 elementType 不同,這是最悲情的情況,我們找到了那個節(jié)點(diǎn),可惜的是這個節(jié)點(diǎn)的 elementType 已經(jīng)變了,那我們也不需要再找了,把 child 及其所有兄弟節(jié)點(diǎn)標(biāo)記刪除,跳出循環(huán)。直接創(chuàng)建一個新的節(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?????????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?????????...
????}
????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)了兩個循環(huán)。
第一輪循環(huán)中邏輯如下:
同時遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。 如果 key 相同。 判斷雙方 elementType 是否相同。 如果相同則復(fù)用 oldFiber 返回。 如果不同則新建 Fiber 返回。 如果 key 不同則直接跳出循環(huán)。

可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時就會跳出循環(huán),換句話說,第一輪循環(huán)里做的事情都是基于 key 相同,主要就是「更新」的工作。
跳出循環(huán)后,要先執(zhí)行兩個判斷
newChildren 已經(jīng)遍歷完了:這種情況說明新的 children 全都已經(jīng)處理完了,只要把 oldFiber 和他所有剩余的兄弟節(jié)點(diǎn)刪除然后返回頭部的 Fiber 即可。 已經(jīng)沒有 oldFiber :這種情況說明 children 有新增的節(jié)點(diǎn),給這些新增的節(jié)點(diǎn)逐一構(gòu)建 Fiber 并鏈接上,然后返回頭部的 Fiber 即可。
如果以上兩種情況都不是,則進(jìn)入第二輪循環(huán)。
在執(zhí)行第二輪循環(huán)之前,先把剩下的舊節(jié)點(diǎn)和他們對應(yīng)的 key 或者 index 做成映射,方便查找。
第二輪循環(huán)沿用了第一輪循環(huán)的 newIdx 變量,說明第二輪循環(huán)是在第一輪循環(huán)結(jié)束的地方開始再次遍歷剩下的 newChildren。
????const?existingChildren?=?mapRemainingChildren(returnFiber,?oldFiber);
????for?(;?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?????????//?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」 來處理節(jié)點(diǎn),在這里需要用 newChild 的 key 去 existingChildren 中找對應(yīng)的 Fiber。
能找到 key 相同的,則說明這個節(jié)點(diǎn)只是位置變了,是可以復(fù)用的。 找不到 key 相同的,則說明這個節(jié)點(diǎn)應(yīng)該是新增的。
不管是復(fù)用還是新增,「updateFromMap」 都會返回一個 newFiber,然后我們需要為這個 newFiber 更新一下它的位置(index),但是僅僅更新這個 Fiber 的 index 還不夠,因?yàn)檫@個 Fiber 有可能是復(fù)用的,如果是復(fù)用的就意味著它已經(jīng)有對應(yīng)的真實(shí) DOM 節(jié)點(diǎn)了,我們還需要復(fù)用它的真實(shí) DOM,因此需要對應(yīng)更新這個 Fiber 的 flag,但是真的需要對每個 Fiber 都去設(shè)置 flag 嗎,我們舉個例子:
//?舊
[<div?key='a'?/>,?'b'?/>,?'c'?/>]
//?新
[<div?key='c'?/>,?'a'?/>,?'b'?/>]
如果按照我們剛剛說的做法,這里的 a, b, c 都會被打上 flag,這樣一來,在 commit 階段,這三個 DOM 都會被移動,可是我們知道,這里顯然只需要移動一個節(jié)點(diǎn)即可,退一萬步說我們移動兩個節(jié)點(diǎn)也比移動所有節(jié)點(diǎn)要來的聰明。
其實(shí)在這個問題上主要就是我們得區(qū)分一下到底哪個節(jié)點(diǎn)才是移動了的,這就需要一個參照點(diǎn),我們要保證在參照點(diǎn)左邊都是已經(jīng)排好順序了的。而這個參照點(diǎn)就是 lastPlacedIndex。有了它,我們在遍歷 newChildren 的時候可能會出現(xiàn)下面兩種情況:
生成(或復(fù)用)的 Fiber 對應(yīng)的老 index < lastPlacedIndex,這就說明這個 Fiber 的位置不對,因?yàn)?lastPlacedIndex 左邊的應(yīng)該全是已經(jīng)遍歷過的 newChild 生成的 Fiber。因此這個 Fiber 是需要被移動的,打上 flag。 如果 Fiber 對應(yīng)的老 index >= lastPlacedIndex,那就說明這個 Fiber 的相對位置是 ok 的,可以不用移動,但是我們需要更新一下參照點(diǎn),把參照點(diǎn)更新成這個 Fiber 對應(yīng)的老 index。
我們舉一個例子:
//?舊
[<div?key='a'?/>,?'b'?/>,?'c'?/>,?'d'?/>]
//?新
[<div?key='c'?/>,?'a'?/>,?'b'?/>,?'d'?/>,?'e'?/>]
lastPlacedIndex 初始值為 0,
首先處理第一個節(jié)點(diǎn) c,給節(jié)點(diǎn) c 的 index 賦值為最新值 0,c.index = 0。
可以看到 c 的 oldIndex 為 2,此時 oldIndex > lastPlacedIndex,無需對 c 做移動,將 lastPlacedIndex 賦值為 2。
此時 lastPlacedIndex = 2。
然后處理節(jié)點(diǎn) a,a.index = 1。
a 的 oldIndex 為 0,此時 oldIndex < lastPlacedIndex,因此需要對 a 打上 Placement 標(biāo)記,lastPlacedIndex 維持不變。
此時 lastPlacedIndex 仍然等于 2。
然后處理節(jié)點(diǎn) b,b.index = 2。
b 的 oldIndex 為 1,此時 oldIndex < lastPlacedIndex,需要對 b 打上 Placement 標(biāo)記,將 lastPlacedIndex 維持不變。
此時 lastPlacedIndex 仍然等于 2。
然后處理節(jié)點(diǎn) d,b.index = 3。
d 的 oldIndex 為 3,此時 oldIndex > lastPlacedIndex,無需對 d 做移動,將 lastPlacedIndex 賦值為 3。
此時 lastPlacedIndex = 3。
然后處理節(jié)點(diǎn) e,e.index = 4。
由于 e 是新建節(jié)點(diǎn),所以 e 的 oldIndex 為 0,此時 oldIndex < lastPlacedIndex,因此需要對 e 打上 Placement 標(biāo)記,lastPlacedIndex 維持不變。
因此最終需要變動位置的節(jié)點(diǎn)是 a b e。
這里可以看到其實(shí)最高效的改動是移動 c 和 e,但是 React 的 diff 邏輯選擇了固定住 c,移動 a b,因此我們平時寫代碼的時候盡量避免把節(jié)點(diǎn)從后面提到前面的操作。
為 newChildren 里的所有 element 都生成了 Fiber 并連接好之后,返回第一個 child ,至此生成子節(jié)點(diǎn)的步驟就完成了。
completeUnitOfWork
在核心流程里我們說到,當(dāng) beginWork 處理到葉子節(jié)點(diǎn),返回 null 的時候就會調(diào)用 「completeUnitOfWork」 函數(shù)。
「completeUnitOfWork」 主要做的事情有兩件:
處理當(dāng)前節(jié)點(diǎn) “歸”操作
處理當(dāng)前節(jié)點(diǎn)
「completeUnitOfWork」 里主要調(diào)用了 「completeWork」 來處理當(dāng)前節(jié)點(diǎn),而在 completeWork 中則是使用了 switch...case... 來處理不同類型的節(jié)點(diǎn),這里我們主要以最常見的 HostComponent 為例。分成首次渲染和非首次渲染兩種情況討論。
mount
當(dāng)是首次渲染時,這里要做的事情主要是:
創(chuàng)建真實(shí) DOM。 如果有子節(jié)點(diǎn)的話將子節(jié)點(diǎn)的真實(shí) DOM 插入到剛剛創(chuàng)建的 DOM 中。 處理真實(shí) DOM 的 props 等。
const?currentHostContext?=?getHostContext();
//?為fiber創(chuàng)建對應(yīng)DOM節(jié)點(diǎn)
const?instance?=?createInstance(
????type,
????newProps,
????rootContainerInstance,
????currentHostContext,
????workInProgress,
??);
//?將子孫DOM節(jié)點(diǎn)插入剛生成的DOM節(jié)點(diǎn)中
appendAllChildren(instance,?workInProgress,?false,?false);
//?DOM節(jié)點(diǎn)賦值給fiber.stateNode
workInProgress.stateNode?=?instance;
//?處理props
if?(
??finalizeInitialChildren(
????instance,
????type,
????newProps,
????rootContainerInstance,
????currentHostContext,
??)
)?{
??markUpdate(workInProgress);
}
update
當(dāng) update 時,F(xiàn)iber 節(jié)點(diǎn)已經(jīng)存在對應(yīng) DOM 節(jié)點(diǎn),所以不需要生成 DOM 節(jié)點(diǎn)。需要做的主要是處理DOM 節(jié)點(diǎn)的 props,這里主要就是一些真實(shí) DOM 的 onClick、onChange等回調(diào)函數(shù)的注冊,style 等,這些處理完之后的 props 也會記錄到 workInProgress.updateQueue 中,并在 commit 階段更新到 DOM 節(jié)點(diǎn)上。
if?(current?!==?null?&&?workInProgress.stateNode?!=?null)?{
??//?update的情況
??updateHostComponent(
????current,
????workInProgress,
????type,
????newProps,
????rootContainerInstance,
??);
}
“歸”
剛剛說到,當(dāng) 「beginWork」 返回值為 null 的時候會進(jìn)入 「completeUnitOfWork」 中,可是我們知道 beginWork 是深度優(yōu)先的更新,也就意味著進(jìn)入 「completeUnitOfWork」 之后必然還需要回到 beginWork 中繼續(xù)處理其他的節(jié)點(diǎn)。
????...
????const?siblingFiber?=?completedWork.sibling;
????if?(siblingFiber?!==?null)?{
??????//?If?there?is?more?work?to?do?in?this?returnFiber,?do?that?next.
??????workInProgress?=?siblingFiber;
??????return;
????}
????//?Otherwise,?return?to?the?parent
????completedWork?=?returnFiber;
????//?Update?the?next?thing?we're?working?on?in?case?something?throws.
????workInProgress?=?completedWork;
可以看到,當(dāng)處理完當(dāng)前節(jié)點(diǎn)之后,React 會判斷當(dāng)前節(jié)點(diǎn)是否具有兄弟節(jié)點(diǎn),如果有的話則將兄弟節(jié)點(diǎn)設(shè)置為當(dāng)前的 workInProgress 回到主流程繼續(xù) 「beginWork。」
而如果沒有兄弟節(jié)點(diǎn)的話,就意味著同父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)都已經(jīng)處理完畢,則接下來就會處理他們的父節(jié)點(diǎn)。
大致流程就是:「beginWork」 執(zhí)行到當(dāng)前節(jié)點(diǎn)沒有 child 的時候,進(jìn)入 「completeUnitOfWork」 處理當(dāng)前節(jié)點(diǎn),處理完后如果當(dāng)前節(jié)點(diǎn)有兄弟節(jié)點(diǎn)則回到 「beginWork」 繼續(xù)處理兄弟節(jié)點(diǎn),如果沒有兄弟節(jié)點(diǎn)則繼續(xù)在 「completeUnitOfWork」 處理當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn),直到“歸”到根結(jié)點(diǎn)上。
三、掛載過程(commitRoot)
effect list
render階段的一個主要工作是收集需要執(zhí)行的 DOM 操作,然后交給 commit階段 來處理,而這些 DOM 操作的具體類型都會保存在 Fiber 節(jié)點(diǎn)的 effectTag 屬性上。
部分 DOM 操作的類型:
//?插入?DOM
export?const?Placement?=?/*????????????????*/?0b00000000000010;
//?更新?DOM
export?const?Update?=?/*???????????????????*/?0b00000000000100;
//?插入并更新?DOM
export?const?PlacementAndUpdate?=?/*???????*/?0b00000000000110;
//?刪除?DOM?節(jié)點(diǎn)
export?const?Deletion?=?/*?????????????????*/?0b00000000001000;
使用二進(jìn)制來表示可以方便地用位運(yùn)算給 effectTag 帶上多個副作用(effect),也可以方便地判斷是否存在某個副作用。例如有個 fiber 節(jié)點(diǎn) effectTag 是 PlacementAndUpdate (0b00000000000110),可以通過按位與運(yùn)算來判斷是否存在Placement:
const?effectTag?=?PlacementAndUpdate;
console.log(effectTag?&?Placement?!==?0);?//?=>?true
在 commit階段 可以像 render階段 那樣遍歷所有 fiber 節(jié)點(diǎn)找出其中的 effectTag,但這樣效率比較低,所以在 render階段 的 completeUnitOfWork 中會把具有 effectTag 的 fiber 節(jié)點(diǎn)連接起來,形成 effectList 鏈表。例如我們有這樣的代碼:
function?App()?{
??const?[count,?setCount]?=?useState(0);
??return?(
????<div?onClick={()?=>?setCount(count?+?1)}>
??????<p>{count}p>
??????<span>{count}span>
????div>
??);
};
我們在 performSyncWorkOnRoot 方法的末尾的 finishedWork 打個斷點(diǎn),然后點(diǎn)擊 div 觸發(fā)一次 setCount 更新:

當(dāng) react 執(zhí)行到斷點(diǎn)這個地方時,我們在控制臺打印一下 finishedWork:

所以此時對應(yīng)的 effectList是:

這里 span 和 p 節(jié)點(diǎn)有 effectTag 是因?yàn)?{count},div 節(jié)點(diǎn)有 effectTag 是因?yàn)橹匦律傻?onClick 函數(shù)。
commit 階段

render階段結(jié)束后,會在performSyncWorkOnRoot()或finishConcurrentRender()中把 fiberRootNode 傳給 commitRoot 方法,開啟 commit階段。
以performSyncWorkOnRoot為例:
function?performSyncWorkOnRoot(root)?{
????//?render階段的入口函數(shù)
????renderRootSync(root,?lanes);
????//?...
????//?commitRoot函數(shù)調(diào)用
????const?finishedWork?=?root.current.alternate;
????root.finishedWork?=?finishedWork;
????root.finishedLanes?=?lanes;
????commitRoot(root);
????//?...省略代碼
}
commit階段的一個主要工作就是遍歷 effectList 并執(zhí)行對應(yīng)的 DOM 操作。commit階段又分為三個子階段:
before mutation階段mutation階段layout階段
下面來看看 commit階段 具體發(fā)生了什么。
進(jìn)入 commitRootImpl 方法時,會先判斷 rootWithPendingPassiveEffects 是否為 null,如果不為 null 就會執(zhí)行 flushPassiveEffects。
function?commitRootImpl(root,?renderPriorityLevel)?{
??do?{
????flushPassiveEffects();
??}?while?(rootWithPendingPassiveEffects?!==?null);
??//?...
}
rootWithPendingPassiveEffects 中的PassiveEffect 是什么意思呢?我們知道如果一個 fiber 節(jié)點(diǎn)的 dom 節(jié)點(diǎn)需要被插入到頁面中,那 fiber.effectTag 就會帶上 Placement effect,類似的,如果一個 FunctionComponent 有 useEffect 需要被執(zhí)行,那它就會帶上 Passive effect。
所以這里的意思是進(jìn)入 commitRoot 時先判斷當(dāng)前是否還有未執(zhí)行的 useEffect,如果有,就執(zhí)行它,也就是說在開啟新一輪的 commit 階段時會先等待上一輪的 useEffect 執(zhí)行完。這其實(shí)在官方文檔里也有一些說明:

接著會重置 render階段使用到的一些全局變量:
function?commitRootImpl(root,?renderPriorityLevel)?{
??do?{
????flushPassiveEffects();
??}?while?(rootWithPendingPassiveEffects?!==?null);
??//?...
??if?(root?===?workInProgressRoot)?{
????workInProgressRoot?=?null;
????workInProgress?=?null;
????workInProgressRootRenderLanes?=?NoLanes;
??}
??
??//?...
}
處理 effect list:
function?commitRootImpl(root,?renderPriorityLevel)?{
??//?...
??let?firstEffect;
??if?(finishedWork.effectTag?>?PerformedWork)?{
????if?(finishedWork.lastEffect?!==?null)?{
??????finishedWork.lastEffect.nextEffect?=?finishedWork;
??????firstEffect?=?finishedWork.firstEffect;
????}?else?{
??????firstEffect?=?finishedWork;
????}
??}?else?{
????firstEffect?=?finishedWork.firstEffect;
??}
??
??//?...
}
上面說過 render階段已經(jīng)把帶有 effectTag 的 fiber 節(jié)點(diǎn)連接形成一條鏈表了,這里再次處理 effect list 是因?yàn)檫@條鏈表目前只有子節(jié)點(diǎn),并沒有掛載根節(jié)點(diǎn)。如果根節(jié)點(diǎn)也存在 effectTag,那么就需要把根節(jié)點(diǎn)拼接到鏈表的末尾,形成一條完整的 effect list:

同時上面的代碼也會取出 firstEffect,也就是第一個需要被處理的 fiber 節(jié)點(diǎn)。接著判斷如果存在 firstEffect,會將 firstEffect 賦值給 nextEffect,開始三個子階段的工作。
function?commitRootImpl(root,?renderPriorityLevel)?{
??//?...
??if?(firstEffect?!==?null)?{
????nextEffect?=?firstEffect;
????
????// beforeMutation 階段:
????//?執(zhí)行?commitBeforeMutationEffects
????
????// mutation 階段:
????//?執(zhí)行?commitMutationEffects
????
????// layout 階段:
????//?執(zhí)行?commitLayoutEffects
??}
??
??//?...
}
beforeMutation階段
beforeMutation階段會執(zhí)行 commitBeforeMutationEffects 方法
function?commitBeforeMutationEffects()?{
??while?(nextEffect?!==?null)?{
????//?...
????
????const?effectTag?=?nextEffect.effectTag;
????if?((effectTag?&?Snapshot)?!==?NoEffect)?{
??????//?...
??????commitBeforeMutationEffectOnFiber(current,?nextEffect);
????}
????//?...
????nextEffect?=?nextEffect.nextEffect;
??}
}
執(zhí)行 getSnapshotBeforeUpdate
在commitBeforeMutationEffects這個方法中會遍歷帶有 effectTag 的 fiber 節(jié)點(diǎn),如果判斷有 Snapshot effectTag 就會調(diào)用 ClassComponent 的 getSnapshotBeforeUpdate 生命周期方法:
function?commitBeforeMutationLifeCycles(
??current:?Fiber?|?null,
??finishedWork:?Fiber,
):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:
????case?ForwardRef:
????case?SimpleMemoComponent:
????case?Block:?{
??????return;
????}
????case?ClassComponent:?{
??????if?(finishedWork.effectTag?&?Snapshot)?{
????????if?(current?!==?null)?{
??????????//?...
??????????const?snapshot?=?instance.getSnapshotBeforeUpdate(/**?...?*/);
??????????//?...
????????}
??????}
??????return;
????}
????case?HostRoot:?{
??????//?...
??????return;
????}
????case?HostComponent:
????case?HostText:
????case?HostPortal:
????case?IncompleteClassComponent:
??????//?Nothing?to?do?for?these?component?types
??????return;
??}
}
調(diào)度 useEffect
再看下 commitBeforeMutationEffects 的剩余部分:
function?commitBeforeMutationEffects()?{
??while?(nextEffect?!==?null)?{
????//?...
????
????if?((effectTag?&?Passive)?!==?NoEffect)?{
??????if?(!rootDoesHavePassiveEffects)?{
????????rootDoesHavePassiveEffects?=?true;
????????scheduleCallback(NormalSchedulerPriority,?()?=>?{
??????????flushPassiveEffects();
??????????return?null;
????????});
??????}
????}
????nextEffect?=?nextEffect.nextEffect;
??}
}
上面提到 flushPassiveEffects 用于執(zhí)行 useEffect 的回調(diào)函數(shù),而這里并不會立即執(zhí)行它,而是把它放在 scheduleCallback 的回調(diào)當(dāng)中,scheduleCallback 方法會以一個優(yōu)先級異步執(zhí)行它的回調(diào)函數(shù)。
所以這段代碼的意思是,如果存在 Passive effect,則把 rootDoesHavePassiveEffects 置為 true,并且調(diào)度 flushPassiveEffects,而整個 commit階段 是「同步執(zhí)行」的,所以 useEffect 的回調(diào)函數(shù)其實(shí)會在 commit階段「完成后」再異步執(zhí)行。
這也跟官方文檔說的對應(yīng)上了:

總結(jié)
在 beforeMutation階段 會:
執(zhí)行getSnapshotBeforeUpdate生命周期方法 「調(diào)度」useEffect
mutation階段
mutation階段會執(zhí)行 commitMutationEffects 方法:
function?commitMutationEffects(root:?FiberRoot,?renderPriorityLevel)?{
??while?(nextEffect?!==?null)?{
????const?effectTag?=?nextEffect.effectTag;
????
????//?如果有?ContentReset,會重置文本節(jié)點(diǎn)
????if?(effectTag?&?ContentReset)?{
??????commitResetTextContent(nextEffect);
????}
????
????//?如果有?Ref,會執(zhí)行?ref?相關(guān)的更新
????if?(effectTag?&?Ref)?{
??????//?...
????}
????const?primaryEffectTag?=
??????effectTag?&?(Placement?|?Update?|?Deletion?|?Hydrating);
????switch?(primaryEffectTag)?{
??????//?如果需要插入節(jié)點(diǎn),會執(zhí)行?commitPlacement
??????case?Placement:?{
????????commitPlacement(nextEffect);
????????nextEffect.effectTag?&=?~Placement;
????????break;
??????}
??????//?如果需要更新節(jié)點(diǎn),會執(zhí)行?commitWork
??????case?Update:?{
????????const?current?=?nextEffect.alternate;
????????commitWork(current,?nextEffect);
????????break;
??????}
??????//?如果需要刪除節(jié)點(diǎn),會執(zhí)行?commitDeletion
??????case?Deletion:?{
????????commitDeletion(root,?nextEffect,?renderPriorityLevel);
????????break;
??????}
??????//?...
????}
????
????//?取出下一個?fiber?節(jié)點(diǎn),進(jìn)入下一次循環(huán)
????nextEffect?=?nextEffect.nextEffect;
??}
}
在這個方法中會遍歷帶有 effectTag 的 fiber 節(jié)點(diǎn),
如果有 ContentReset,會重置文本節(jié)點(diǎn) 如果有 Ref,會執(zhí)行 ref 相關(guān)的操作 增刪改 如果需要插入節(jié)點(diǎn),會執(zhí)行 commitPlacement 如果需要更新節(jié)點(diǎn),會執(zhí)行 commitWork 如果需要刪除節(jié)點(diǎn),會執(zhí)行 commitDeletion ...
commitPlacement
在 commitPlacement 方法中,會先找到距離最近的 host 類型父節(jié)點(diǎn)和距離最近的 host 類型兄弟節(jié)點(diǎn),然后根據(jù) host 父 fiber 節(jié)點(diǎn)的類型取出對應(yīng)的 DOM 節(jié)點(diǎn),接著根據(jù)是否 container 來執(zhí)行 insertOrAppendPlacementNodeIntoContainer 或 insertOrAppendPlacementNode。
function?commitPlacement(finishedWork:?Fiber):?void?{
??//?找到?host?父?fiber?節(jié)點(diǎn)
??const?parentFiber?=?getHostParentFiber(finishedWork);
??let?parent;
??let?isContainer;
??const?parentStateNode?=?parentFiber.stateNode;
??
??//?根據(jù)?host?父?fiber?節(jié)點(diǎn)的類型,取出對應(yīng)的?DOM?節(jié)點(diǎn)
??switch?(parentFiber.tag)?{
????case?HostComponent:
??????parent?=?parentStateNode;
??????isContainer?=?false;
??????break;
????case?HostRoot:
??????parent?=?parentStateNode.containerInfo;
??????isContainer?=?true;
??????break;
????case?HostPortal:
??????parent?=?parentStateNode.containerInfo;
??????isContainer?=?true;
??????break;
????//?...
??}
??//?...
??//?獲取?host?兄弟節(jié)點(diǎn)
??const?before?=?getHostSibling(finishedWork);
??
??//?根據(jù)是否?container?來決定執(zhí)行哪個方法
??if?(isContainer)?{
????insertOrAppendPlacementNodeIntoContainer(finishedWork,?before,?parent);
??}?else?{
????insertOrAppendPlacementNode(finishedWork,?before,?parent);
??}
}
為什么需要先找到 host 父 fiber 節(jié)點(diǎn)和 host 兄弟 fiber 節(jié)點(diǎn)?
我們知道在DOM中插入一個節(jié)點(diǎn)有兩種方式:
parentNode.appendChild(newNode)parentNode.insertBefore(newNode, referenceNode)
無論哪種方式都需要找到它的父 DOM 節(jié)點(diǎn),而如果需要 insertBefore 則還需要找到它的兄弟 DOM 節(jié)點(diǎn)。
另外,為什么需要找 host 類型的父節(jié)點(diǎn)和兄弟節(jié)點(diǎn)呢?這是因?yàn)樽罱母?「fiber」 節(jié)點(diǎn)不一定就是最近的父 「DOM」 節(jié)點(diǎn),同理,最近的兄弟 「fiber」 節(jié)點(diǎn)不一樣是最近的兄弟 「DOM」 節(jié)點(diǎn)。例如,我們的代碼長這樣:
function?Item()?{
??return?
}
function?App()?{
??return?(
????
??????
??????
????
??);
}
它對應(yīng)的 fiber 樹和 dom 樹分別是:

可以看到, fiber 樹和 don 樹并不是一一對應(yīng)的:
p 的 fiber 節(jié)點(diǎn)的父節(jié)點(diǎn)是 Item,而 p 的 dom 節(jié)點(diǎn)的父節(jié)點(diǎn)是 div p 的 fiber 節(jié)點(diǎn)沒有兄弟節(jié)點(diǎn),而 p 的 dom 節(jié)點(diǎn)有個兄弟節(jié)點(diǎn)是 span
上面的insertOrAppendPlacementNodeIntoContainer 和 insertOrAppendPlacementNode做的事情差不多,都會判斷是否有 before,如果有,則執(zhí)行 insertBefore,沒有則執(zhí)行 appendChild:
function?insertOrAppendPlacementNode(
??node:?Fiber,
??before:??Instance,
??parent:?Instance,
):?void?{
??//?...
??if?(before)?{
????insertBefore(parent,?stateNode,?before);
??}?else?{
????appendChild(parent,?stateNode);
??}
??//?...
}
appendChild 和 insertBefore 都來自于 ReactFiberHostConfig。
這里的 ReactFiberHostConfig 在源碼里其實(shí)只是一個空殼,最終需要被特定環(huán)境的 renderer 來填充,例如在我們平常使用 ReactDOM 時,ReactFiberHostConfig 會被 ReactDOM 的 ReactDOMHostConfig 來填充:
export?function?appendChild(
??parentInstance:?Instance,
??child:?Instance?|?TextInstance,
):?void?{
??parentInstance.appendChild(child);
}
export?function?insertBefore(
??parentInstance:?Instance,
??child:?Instance?|?TextInstance,
??beforeChild:?Instance?|?TextInstance?|?SuspenseInstance,
):?void?{
??parentInstance.insertBefore(child,?beforeChild);
}
可以看到,其實(shí)最終就是執(zhí)行 dom 節(jié)點(diǎn)的 appendChild 或 insertBefore 方法。
?React 倉庫其他 renderer 的 hostConfig:
?

BTW,umijs/hox(https://github.com/umijs/hox) 是一個基于 hooks 的全局狀態(tài)管理工具,它跟其他基于 hooks 的狀態(tài)管理工具很大的一個不同點(diǎn)是, hox 不需要我們手動掛載 就能直接使用 model:
//?counterModel.js
import?{?useState?}?from?"react";
import?{?createModel?}?from?"hox";
function?useCounter()?{
??const?[count,?setCount]?=?useState(0);
??const?decrement?=?()?=>?setCount(count?-?1);
??const?increment?=?()?=>?setCount(count?+?1);
??return?{
????count,
????decrement,
????increment
??};
}
export?default?createModel(useCounter);
//?index.jsx
import?useCounterModel?from?"./counterModel";
function?App(props)?{
??const?counter?=?useCounterModel();
??return?(
????
??????{counter.count}
??????
????
??);
}
其實(shí)它是在調(diào)用 createModel 時把 hooks 的執(zhí)行掛在了它自定義的一個 renderer 里,對應(yīng)的 hostConfig 都是空函數(shù),因?yàn)樗⒉恍枰獔?zhí)行真正的渲染,只是用來執(zhí)行 hooks 而已:
import?ReactReconciler?from?"react-reconciler";
import?{?ReactElement?}?from?"react";
const?hostConfig?=?{
??now:?Date.now,
??getRootHostContext:?()?=>?({}),
??prepareForCommit:?()?=>?{},
??resetAfterCommit:?()?=>?{},
??getChildHostContext:?()?=>?({}),
??shouldSetTextContent:?()?=>?true,
??createInstance:?()?=>?{},
??createTextInstance:?()?=>?{},
??appendInitialChild:?()?=>?{},
??appendChild:?()?=>?{},
??finalizeInitialChildren:?()?=>?{},
??supportsMutation:?true,
??appendChildToContainer:?()?=>?{},
??prepareUpdate:?()?=>?true,
??commitUpdate:?()?=>?{},
??commitTextUpdate:?()?=>?{},
??removeChild:?()?=>?{}
};
const?reconciler?=?ReactReconciler(hostConfig?as?any);
export?function?render(reactElement:?ReactElement)?{
??const?container?=?reconciler.createContainer(null,?false,?false);
??return?reconciler.updateContainer(reactElement,?container,?null,?null);
}
commitWork
commitWork 用于更新節(jié)點(diǎn)。在這個方法中會根據(jù) fiber 節(jié)點(diǎn)的類型進(jìn)行不同的操作:
function?commitWork(current:?Fiber?|?null,?finishedWork:?Fiber):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:
????case?ForwardRef:
????case?MemoComponent:
????case?SimpleMemoComponent:
????case?Block:?{
??????//?...
??????return;
????}
????case?ClassComponent:?{
??????return;
????}
????case?HostComponent:?{
??????//?...
??????return;
????}
????case?HostText:?{
??????//?...
??????return;
????}
????//?case...
??}
}
對于 FunctionComponent,更新時會執(zhí)行 commitHookEffectListUnmount(``HookLayout | HookHasEffect, finishedWork):
function?commitHookEffectListUnmount(tag:?number,?finishedWork:?Fiber)?{
??const?updateQueue?=?finishedWork.updateQueue;
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????if?((effect.tag?&?tag)?===?tag)?{
????????const?destroy?=?effect.destroy;
????????effect.destroy?=?undefined;
????????if?(destroy?!==?undefined)?{
??????????destroy();
????????}
??????}
??????effect?=?effect.next;
????}?while?(effect?!==?firstEffect);
??}
}
里面會遍歷 effect list,判斷 effect.tag 是否存在參數(shù)傳入的 tag 類型,在這個場景里被傳入 tag 參數(shù)是 HookLayout,也就是 useLayoutEffect 對應(yīng)的 effect tag。所以這里的意思是遍歷 effect list,如果存在 useLayoutEffect 的 effect tag,則執(zhí)行它的銷毀函數(shù)(即 useLayoutEffect 的回調(diào)函數(shù)的返回值)。
如果 fiber 節(jié)點(diǎn)的類型是 HostComponent,也就是 dom 節(jié)點(diǎn)對應(yīng)的 fiber 節(jié)點(diǎn),更新時會執(zhí)行 commitUpdate 方法:
function?commitWork(current:?Fiber?|?null,?finishedWork:?Fiber):?void?{
??switch?(finishedWork.tag)?{
????//?...
????case?HostComponent:?{
??????const?instance:?Instance?=?finishedWork.stateNode;
??????if?(instance?!=?null)?{
????????//?...
????????const?updatePayload:?null?|?UpdatePayload?=?(finishedWork.updateQueue:?any);
????????//?...
????????if?(updatePayload?!==?null)?{
??????????commitUpdate(
????????????instance,
????????????updatePayload,
????????????type,
????????????oldProps,
????????????newProps,
????????????finishedWork,
??????????);
????????}
????????if?(enableDeprecatedFlareAPI)?{
??????????const?prevListeners?=?oldProps.DEPRECATED_flareListeners;
??????????const?nextListeners?=?newProps.DEPRECATED_flareListeners;
??????????if?(prevListeners?!==?nextListeners)?{
????????????updateDeprecatedEventListeners(nextListeners,?finishedWork,?null);
??????????}
????????}
??????}
??????return;
????}
????//?...
??}
}
commitUpdate 方法接收的參數(shù)中有個 updatePayload,它來自于 fiber.updateQueue 屬性,對于類型為 HostComponent 的 fiber 節(jié)點(diǎn)來說,它的 updateQueue 屬性是一個數(shù)組,表示這個 dom 節(jié)點(diǎn)的屬性變更,例如一個 dom 節(jié)點(diǎn)在某次更新中它的 a 屬性需要從 react 更新為 vue,b 屬性需要從 byte 更新為 dance,那這個 dom 節(jié)點(diǎn)的 fiber 節(jié)點(diǎn)的 updateQueue 就長這樣 ['a', 'vue', 'b', 'dance'],也就是說第 i 項(xiàng)是屬性 key,第 i + 1 項(xiàng)是屬性 value。
commitUpdate 方法同樣來自 ReactFiberHostConfig:
export?function?commitUpdate(
??domElement:?Instance,
??updatePayload:?Array,
??type:?string,
??oldProps:?Props,
??newProps:?Props,
??internalInstanceHandle:?Object,
):?void?{
??//?...
??updateProperties(domElement,?updatePayload,?type,?oldProps,?newProps);
}
它最終會調(diào)用 updateDOMProperties 來更新 dom 屬性:
function?updateDOMProperties(
??domElement:?Element,
??updatePayload:?Array,
??wasCustomComponentTag:?boolean,
??isCustomComponentTag:?boolean,
):?void?{
??for?(let?i?=?0;?i?????const?propKey?=?updatePayload[i];
????const?propValue?=?updatePayload[i?+?1];
????if?(propKey?===?STYLE)?{
??????setValueForStyles(domElement,?propValue);
????}?else?if?(propKey?===?DANGEROUSLY_SET_INNER_HTML)?{
??????setInnerHTML(domElement,?propValue);
????}?else?if?(propKey?===?CHILDREN)?{
??????setTextContent(domElement,?propValue);
????}?else?{
??????setValueForProperty(domElement,?propKey,?propValue,?isCustomComponentTag);
????}
??}
}
這里的 i 和 i + 1 就對應(yīng)著上面提到的 updateQueue 的數(shù)據(jù)結(jié)構(gòu),第 i 項(xiàng)是屬性 key,第 i + 1 項(xiàng)是屬性 value。
commitDeletion
commitDeletion 用于執(zhí)行刪除操作:
function?commitDeletion(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??//?...
??unmountHostComponents(finishedRoot,?current,?renderPriorityLevel);
??//?...
}
在 unmountHostComponents 中核心是遍歷節(jié)點(diǎn)調(diào)用 commitUnmount 方法。在這個方法中會根據(jù) fiber 節(jié)點(diǎn)的類型做不同的處理。
對于 FunctionComponent,會注冊它的 useEffect 銷毀函數(shù),其實(shí)就是把這個 effect 推進(jìn) pendingPassiveHookEffectsUnmount 這個數(shù)組中,便于「后續(xù)」取出來執(zhí)行銷毀函數(shù)。
function?commitUnmount(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??switch?(current.tag)?{
????case?FunctionComponent:?{
??????//?...
??????enqueuePendingPassiveHookEffectUnmount(current,?effect);
??????//?...
??????return;
????}
??}
}
對于 ClassComponent,會執(zhí)行它的 componentWillUnmount 方法:
function?commitUnmount(
??finishedRoot:?FiberRoot,
??current:?Fiber,
??renderPriorityLevel:?ReactPriorityLevel,
):?void?{
??switch?(current.tag)?{
????case?ClassComponent:?{
??????const?instance?=?current.stateNode;
??????if?(typeof?instance.componentWillUnmount?===?'function')?{
????????safelyCallComponentWillUnmount(current,?instance);
??????}
??????return;
????}
??}
}
layout階段
layout階段 會執(zhí)行 commitLayoutEffects 方法,里面核心是執(zhí)行commitLifeCycles方法。在這個方法中會根據(jù) fiber 節(jié)點(diǎn)的類型執(zhí)行不同的處理。
function?commitLifeCycles(
??finishedRoot:?FiberRoot,
??current:?Fiber?|?null,
??finishedWork:?Fiber,
??committedLanes:?Lanes,
):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:?{
??????commitHookEffectListMount(HookLayout?|?HookHasEffect,?finishedWork);
??????schedulePassiveEffects(finishedWork);
??????return;
????}
??}
}
對于 FunctionComponent,會把 HookLayout 這個 tag 類型傳給 commitHookEffectListMount 方法,也就是說這里會執(zhí)行 useLayoutEffect 的回調(diào)函數(shù)。
接著會執(zhí)行 schedulePassiveEffects 方法:
function?schedulePassiveEffects(finishedWork:?Fiber)?{
??const?updateQueue:?FunctionComponentUpdateQueue?|?null?=?(finishedWork.updateQueue:?any);
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????const?{next,?tag}?=?effect;
??????if?(
????????(tag?&?HookPassive)?!==?NoHookEffect?&&
????????(tag?&?HookHasEffect)?!==?NoHookEffect
??????)?{
????????enqueuePendingPassiveHookEffectUnmount(finishedWork,?effect);
????????enqueuePendingPassiveHookEffectMount(finishedWork,?effect);
??????}
??????effect?=?next;
????}?while?(effect?!==?firstEffect);
??}
}
在這里會分別注冊 useEffect 銷毀函數(shù)和回調(diào)函數(shù),其實(shí)也就是把 effect 分別推進(jìn) pendingPassiveHookEffectsUnmount 和 pendingPassiveHookEffectsMount 這兩個數(shù)組中,用于「后續(xù)」取出來執(zhí)行。
對于 ClassComponent,如果 current 為空,也就是這個節(jié)點(diǎn)是首次 render,則會執(zhí)行它的 componentDidMount 生命周期方法,否則會執(zhí)行 componentDidUpdate 方法:
function?commitLifeCycles(
??finishedRoot:?FiberRoot,
??current:?Fiber?|?null,
??finishedWork:?Fiber,
??committedLanes:?Lanes,
):?void?{
??switch?(finishedWork.tag)?{
????case?ClassComponent:?{
??????const?instance?=?finishedWork.stateNode;
??????if?(finishedWork.effectTag?&?Update)?{
????????if?(current?===?null)?{
??????????instance.componentDidMount();
????????}?else?{
??????????instance.componentDidUpdate(
????????????prevProps,
????????????prevState,
????????????instance.__reactInternalSnapshotBeforeUpdate,
??????????);
????????}
??????}
??????return;
????}
??}
}
commit 階段總結(jié)
等待執(zhí)行完上一輪渲染的 useEffect 重置一些全局變量(如:workInProgressRoot) 更新副作用列表 effect list。根節(jié)點(diǎn)的副作用列表是不包括自身的,如果根節(jié)點(diǎn)有副作用, 則需要把根節(jié)點(diǎn)添加到副作用列表的末尾 渲染 執(zhí)行 componentDidMount、componentDidUpdate 執(zhí)行 useLayoutEffect 的回調(diào)函數(shù) 注冊 useEffect 的回調(diào)函數(shù)和銷毀函數(shù),等 commit 階段結(jié)束后再異步執(zhí)行
執(zhí)行 DOM 增刪改 執(zhí)行 useLayoutEffect 的銷毀函數(shù)
執(zhí)行 getSnapshotBeforeUpdate 調(diào)度 useEffect
beforeMutation mutation 切換當(dāng)前 fiber 樹(root.current = finishedWork),使得 fiberRoot 的 current 指向的是當(dāng)前頁面展示的 fiber 樹。 layout
瀏覽
82評論圖片表情
人人草人人摸人人干
|
一级A片黄片
|
思思热免费视频在线观看
|
青榴视频在线观看
|
美女av在线播放
美女高潮喷水影院
|
