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

專業(yè)從事成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè),高端網(wǎng)站制作設(shè)計(jì),小程序開發(fā),網(wǎng)站推廣的成都做網(wǎng)站的公司。優(yōu)秀技術(shù)團(tuán)隊(duì)竭力真誠服務(wù),采用HTML5+CSS3前端渲染技術(shù),自適應(yīng)網(wǎng)站建設(shè),讓網(wǎng)站在手機(jī)、平板、PC、微信下都能呈現(xiàn)。建站過程建立專項(xiàng)小組,與您實(shí)時(shí)在線互動,隨時(shí)提供解決方案,暢聊想法和感受。
一、前置知識
聲明式渲染
『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機(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 < 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);
可以看到在這個例子中,聲明式寫法以 HTML 語法直接告訴機(jī)器,我需要的視圖應(yīng)該是長這個樣子,然后具體的 DOM 操作全部交由機(jī)器去完成。開發(fā)者只需要專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。
這便是聲明式渲染。
聲明式渲染是現(xiàn)代前端框架的比較普遍的設(shè)計(jì)思路。
JSX 和 ReactElement
相信大家最初學(xué) React 的時(shí)候都有這樣的疑問,為什么我們能夠以類似 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
{ onCountClick()}}>- count: {count}
- }
可以看到,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
- text
- // Count.jsx
最終形成的 Fiber 樹結(jié)構(gòu)為:
總結(jié)一下,我們編寫的 jsx 首先會形成 element ,然后在 render 過程中每個 element 都會生成對應(yīng)的 Fiber,最終形成 Fiber 樹。
jsx -> element -> Fiber
下面我們正式介紹一下 render 的過程,看看 Fiber 是如何生成并形成 Fiber 樹的。
二、渲染(render)過程
核心流程
通常 React 運(yùn)行時(shí)會有兩個 Fiber 樹,一個是根據(jù)當(dāng)前最新組件狀態(tài)構(gòu)建出來的,另一個則是上一次構(gòu)建出來的 Fiber 樹,當(dāng)然如果是首次渲染就沒有上一次的 Fiber 樹,這時(shí)就只有一個了。簡單來說,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)的時(shí)候說過,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),這時(shí)候就需要“歸”了。
「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
在介紹概覽的時(shí)候說過,React 通常會同時(shí)存在兩個 Fiber 樹,一個是當(dāng)前視圖對應(yīng)的,一個則是根據(jù)最新狀態(tài)正在構(gòu)建中的。這兩棵樹的節(jié)點(diǎn)一一對應(yīng),我們用 current 來代表前者,我們不難發(fā)現(xiàn),當(dāng)首次渲染的時(shí)候,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 重新渲染時(shí)這個 B 組件是符合 React 優(yōu)化條件的。但結(jié)果是,每次 Demo 重新渲染都會導(dǎo)致 B 組件重新渲染。每次渲染時(shí) B 組件的 props 看似沒發(fā)生變化,但由于 Demo 重新執(zhí)行后會生成全新的 B 組件(下面會介紹),所以新舊 B 組件的 props 肯定也是不同的。
- function App() {
- return
- }
- function Demo() {
- const [v, setV] = useState();
- return (
- );
- }
那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來,B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因?yàn)椴还?Demo 組件狀態(tài)怎么變化都不會影響到 App 組件,因此 App 和 B 組件就只會在首次渲染時(shí)會執(zhí)行一遍,也就是說 Demo 獲取到的 props.children 的引用一直都是指向同一個對象,這樣一來 B 組件的 props 也就不會變化了。
- function App() {
- return
- }
- function Demo(props) {
- const [v, setV] = useState();
- return (
- );
- }
更新當(dāng)前節(jié)點(diǎn)
通過上面的解析我們知道,當(dāng)不走優(yōu)化邏輯時(shí) 「beginWork」 使用大量的 switch...case 來分別處理不同類型的組件,下來我們以我們熟悉的 Function Component 為例。
「核心就是通過調(diào)用函數(shù)組件,得到組件的返回的 element?!?/p>
類似地,對于類組件,則是調(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 < 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)了兩個循環(huán)。
第一輪循環(huán)中邏輯如下:
- 同時(shí)遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。
- 如果 key 相同。
- 判斷雙方 elementType 是否相同。
- 如果相同則復(fù)用 oldFiber 返回。
- 如果不同則新建 Fiber 返回。
- 如果 key 不同則直接跳出循環(huán)。
可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時(shí)就會跳出循環(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 < 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」 來處理節(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 嗎,我們舉個例子:
- // 舊
- [, , ]
- // 新
- [, , ]
如果按照我們剛剛說的做法,這里的 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 的時(shí)候可能會出現(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。
我們舉一個例子:
- // 舊
- [, , , ]
- // 新
- [, , , , ]
lastPlacedIndex 初始值為 0,
首先處理
文章題目:一文讀懂React組件渲染核心原理
分享鏈接:http://www.fisionsoft.com.cn/article/dpjoepc.html


咨詢
建站咨詢
