2023.04.30 更新前端面試問題總結(jié)(7道題)

2023.04.26 - 2023.04.30 更新前端面試問題總結(jié)(7道題)
獲取更多面試問題可以訪問
github 地址: https://github.com/pro-collection/interview-question/issuesgitee 地址: https://gitee.com/yanleweb/interview-question/issues

目錄:

  • 中級開發(fā)者相關(guān)問題【共計 1 道題】

    • 317.[React] react 是如何實現(xiàn)頁面的快速響應(yīng)甜刻?【熱度: 696】【web框架】
  • 高級開發(fā)者相關(guān)問題【共計 5 道題】

    • 318.[React] React15 架構(gòu)存在什么樣的問題?【熱度: 1,613】【web框架】
    • 319.[React] React16 是什么樣的架構(gòu)特點?【熱度: 2,403】【web框架】
    • 322.[React] fiber 架構(gòu) 的工作原理?【熱度: 1,774】【web框架】
    • 323.[React] Fiber的含義與數(shù)據(jù)結(jié)構(gòu)【熱度: 1,778】【web框架】
    • 324.[React] render 階段的執(zhí)行過程【熱度: 1,793】【web框架】
  • 資深開發(fā)者相關(guān)問題【共計 1 道題】

    • 320.[React] React Reconciler 為何要采用 fiber 架構(gòu)?【熱度: 1,794】【web框架】

中級開發(fā)者相關(guān)問題【共計 1 道題】

317.[React] react 是如何實現(xiàn)頁面的快速響應(yīng)?【熱度: 696】【web框架】

關(guān)鍵詞:react 快速響應(yīng)實現(xiàn)、react 可中斷更新学搜、react IO瓶頸、react CPU瓶頸

react 是如何實現(xiàn)快速響應(yīng)的论衍?

我們?nèi)粘J褂肁pp瑞佩,瀏覽網(wǎng)頁時,有兩類場景會制約快速響應(yīng):

當(dāng)遇到大計算量的操作或者設(shè)備性能不足使頁面掉幀坯台,導(dǎo)致卡頓炬丸。

發(fā)送網(wǎng)絡(luò)請求后,由于需要等待數(shù)據(jù)返回才能進(jìn)一步操作導(dǎo)致不能快速響應(yīng)蜒蕾。

這兩類場景可以概括為:

  • CPU的瓶頸
  • IO的瓶頸

CPU的瓶頸

主流瀏覽器刷新頻率為60Hz稠炬,即每(1000ms / 60Hz)16.6ms瀏覽器刷新一次。

我們知道咪啡,JS可以操作DOM首启,GUI渲染線程與JS線程是互斥的。所以JS腳本執(zhí)行和瀏覽器布局瑟匆、繪制不能同時執(zhí)行闽坡。

在每16.6ms時間內(nèi),需要完成如下工作: JS腳本執(zhí)行 ----- 樣式布局 ----- 樣式繪制

當(dāng)JS執(zhí)行時間過長愁溜,超出了16.6ms,這次刷新就沒有時間執(zhí)行樣式布局和樣式繪制了外厂。

比如我們可以通過一個循環(huán)冕象, 渲染列表 3000 個組件, 那么這種渲染時間汁蝶, 就肯定是遠(yuǎn)超過 16.6 ms 的渐扮, 頁面就會感覺到卡頓论悴。

如何解決這個問題呢?

答案是:在瀏覽器每一幀的時間中墓律,預(yù)留一些時間給JS線程膀估,React利用這部分時間更新組件(可以看到,在源碼中耻讽,預(yù)留的初始時間是5ms)察纯。源碼位置: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L119

當(dāng)預(yù)留的時間不夠用時,React將線程控制權(quán)交還給瀏覽器使其有時間渲染UI针肥,React則等待下一幀時間到來繼續(xù)被中斷的工作饼记。

這種將長任務(wù)分拆到每一幀中,像螞蟻搬家一樣一次執(zhí)行一小段任務(wù)的操作慰枕,被稱為時間切片(time slice)

所以具则,解決CPU瓶頸的關(guān)鍵是實現(xiàn)時間切片,而時間切片的關(guān)鍵是:將同步的更新變?yōu)榭芍袛嗟漠惒礁隆?/strong>

IO的瓶頸

網(wǎng)絡(luò)延遲是前端開發(fā)者無法解決的具帮。如何在網(wǎng)絡(luò)延遲客觀存在的情況下博肋,減少用戶對網(wǎng)絡(luò)延遲的感知?

簡單點兒來說蜂厅, 就是在點擊頁面跳轉(zhuǎn)的是時候提前去加載下一個頁面的內(nèi)容匪凡。 或者在當(dāng)前頁面 hold .5s 左右時間, 利用這個時間去加載下一個頁面的內(nèi)容葛峻。從而達(dá)到下一個頁面的快速交互

React實現(xiàn)了 Suspense 功能及配套的 hook——useDeferredValue锹雏。

而在源碼內(nèi)部,為了支持這些特性术奖,同樣需要將同步的更新變?yōu)榭芍袛嗟漠惒礁隆?/strong>

高級開發(fā)者相關(guān)問題【共計 5 道題】

318.[React] React15 架構(gòu)存在什么樣的問題礁遵?【熱度: 1,613】【web框架】

關(guān)鍵詞:react15 架構(gòu)、react 架構(gòu)采记、react Reconciler佣耐、react 渲染器、react 協(xié)調(diào)器

React15 架構(gòu)可以分為兩層:

  • Reconciler(協(xié)調(diào)器)—— 負(fù)責(zé)找出變化的組件
  • Renderer(渲染器)—— 負(fù)責(zé)將變化的組件渲染到頁面上

Reconciler(協(xié)調(diào)器)

我們知道唧龄,在React中可以通過 this.setState兼砖、this.forceUpdate、ReactDOM.render 等API觸發(fā)更新既棺。

每當(dāng)有更新發(fā)生時讽挟,Reconciler會做如下工作:

  • 調(diào)用函數(shù)組件、或class組件的render方法丸冕,將返回的JSX轉(zhuǎn)化為虛擬DOM
  • 將虛擬DOM和上次更新時的虛擬DOM對比
  • 通過對比找出本次更新中變化的虛擬DOM
  • 通知Renderer將變化的虛擬DOM渲染到頁面上

Renderer(渲染器)

由于React支持跨平臺耽梅,所以不同平臺有不同的Renderer。我們前端最熟悉的是負(fù)責(zé)在瀏覽器環(huán)境渲染的Renderer —— ReactDOM

除此之外胖烛,還有:

  • ReactNative 渲染器眼姐,渲染App原生組件
  • ReactTest 渲染器诅迷,渲染出純Js對象用于測試
  • ReactArt 渲染器,渲染到Canvas, SVG 或 VML (IE8)

在每次更新發(fā)生時众旗,Renderer接到 Reconciler 通知罢杉,將變化的組件渲染在當(dāng)前宿主環(huán)境。

React15 架構(gòu)的缺點

react15 是通過遞歸去更新組件的

在 Reconciler 中贡歧,mount的組件會調(diào)用 mountComponent (opens new window)滩租,update 的組件會調(diào)用 updateComponent (opens new window)。這兩個方法都會遞歸更新子組件艘款。

由于遞歸執(zhí)行持际,所以更新一旦開始,中途就無法中斷哗咆。當(dāng)層級很深時蜘欲,遞歸更新時間超過了16ms,用戶交互就會卡頓晌柬。

本質(zhì)上說是因為 遞歸 的架構(gòu)姥份, 是不允許中斷的, 因為 react 希望有更好的渲染性能年碘,那么面對大規(guī)模 dom diff 更新渲染的時候澈歉, 就不能讓每一遞歸時間超過 16 ms。遞歸是做不到這個功能的屿衅。 所以只有重寫 react15 架構(gòu)埃难。引入了 react16 fiber 架構(gòu)。

319.[React] React16 是什么樣的架構(gòu)特點涤久?【熱度: 2,403】【web框架】

關(guān)鍵詞:react16 架構(gòu)涡尘、react Reconciler、react fiber响迂、react 渲染器考抄、react 協(xié)調(diào)器

React16架構(gòu)可以分為三層:

Scheduler(調(diào)度器)—— 調(diào)度任務(wù)的優(yōu)先級,高優(yōu)任務(wù)優(yōu)先進(jìn)入Reconciler
Reconciler(協(xié)調(diào)器)—— 負(fù)責(zé)找出變化的組件
Renderer(渲染器)—— 負(fù)責(zé)將變化的組件渲染到頁面上可以看到蔗彤,相較于React15川梅,React16中新增了Scheduler(調(diào)度器)。

Scheduler(調(diào)度器)

以瀏覽器是否有剩余時間作為任務(wù)中斷的標(biāo)準(zhǔn)然遏,那么需要一種機(jī)制贫途,當(dāng)瀏覽器有剩余時間時通知我們

其實部分瀏覽器已經(jīng)實現(xiàn)了這個API待侵,這就是 requestIdleCallback (opens new window)潮饱。但是由于以下因素,React放棄使用:

  • 瀏覽器兼容性
  • 觸發(fā)頻率不穩(wěn)定诫给,受很多因素影響香拉。比如當(dāng)我們的瀏覽器切換tab后,之前tab注冊的 requestIdleCallback 觸發(fā)的頻率會變得很低

基于以上原因中狂,React實現(xiàn)了功能更完備的 requestIdleCallback polyfill凫碌,這就是Scheduler。除了在空閑時觸發(fā)回調(diào)的功能外胃榕,Scheduler 還提供了多種調(diào)度優(yōu)先級供任務(wù)設(shè)置盛险。

Scheduler (opens new window) 是獨立于React的庫

Reconciler(協(xié)調(diào)器)

在 React15 中 Reconciler 是遞歸處理虛擬DOM的

在 React16 中更新工作從遞歸變成了可以中斷的循環(huán)過程。每次循環(huán)都會調(diào)用 shouldYield 判斷當(dāng)前是否有剩余時間勋又。

/** @noinline */function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}

那么React16是如何解決中斷更新時DOM渲染不完全的問題呢苦掘?

在React16中,Reconciler與Renderer不再是交替工作楔壤。當(dāng)Scheduler將任務(wù)交給Reconciler后鹤啡,Reconciler會為變化的虛擬DOM打上代表增/刪/更新的標(biāo)記;

全部標(biāo)記可以見這里: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactSideEffectTags.js

整個Scheduler與 Reconciler 的工作都在內(nèi)存中進(jìn)行蹲嚣。只有當(dāng)所有組件都完成Reconciler的工作递瑰,才會統(tǒng)一交給Renderer。

可以看這里 react16 對 Reconciler 的解釋:https://zh-hans.legacy.reactjs.org/docs/codebase-overview.html#fiber-reconciler

Reconciler 內(nèi)部采用了 Fiber 的架構(gòu)隙畜。

Renderer(渲染器)

Renderer根據(jù)Reconciler為虛擬DOM打的標(biāo)記抖部,同步執(zhí)行對應(yīng)的DOM操作。

參考資料

  • https://react.iamkasong.com/preparation/newConstructure.html#react16%E6%9E%B6%E6%9E%84

322.[React] fiber 架構(gòu) 的工作原理议惰?【熱度: 1,774】【web框架】

關(guān)鍵詞:react16 架構(gòu)慎颗、react Reconciler、react fiber言询、react 協(xié)調(diào)器

雙緩存Fiber樹

如果當(dāng)前幀畫面計算量比較大俯萎,導(dǎo)致清除上一幀畫面到繪制當(dāng)前幀畫面之間有較長間隙,就會出現(xiàn)白屏倍试。

為了解決這個問題讯屈, 就有了圖像處理中的雙緩存技術(shù)

雙緩存是一種技術(shù)县习,用于在圖像處理中減少閃爍和圖像模糊等視覺問題涮母。在使用雙緩存時,圖像處理器會將圖像繪制到一個“后臺緩存”中躁愿,而不是直接繪制到屏幕上叛本。一旦繪制完成,新的圖像將與當(dāng)前顯示的圖像交換彤钟,使得新圖像無縫地顯示在屏幕上来候,避免了閃爍和模糊的問題。因此逸雹,雙緩存有助于提高圖像處理的質(zhì)量和可靠性营搅,特別是在高速顯示和實時處理應(yīng)用中云挟。

React使用“雙緩存”來完成Fiber樹的構(gòu)建與替換——對應(yīng)著DOM樹的創(chuàng)建與更新。

在React中最多會同時存在兩棵Fiber樹转质。當(dāng)前屏幕上顯示內(nèi)容對應(yīng)的Fiber樹稱為current Fiber樹园欣,正在內(nèi)存中構(gòu)建的Fiber樹稱為workInProgress Fiber樹。

React Fiber 的雙緩存機(jī)制是一種優(yōu)化技術(shù)休蟹,用于在 UI 更新過程中避免視覺問題沸枯,如閃爍、撕裂和卡頓等赂弓。React Double Buffer 在 React Fiber 內(nèi)部實現(xiàn)了兩個緩存區(qū)域:當(dāng)前顯示的緩存(Current Buffer)和等待顯示的緩存(Work Buffer)绑榴。

currentFiber.alternate === workInProgressFiber;workInProgressFiber.alternate === currentFiber;

當(dāng)應(yīng)用程序狀態(tài)發(fā)生更改,并需要更新 UI 時盈魁,React Fiber 首先在 Work Buffer 中執(zhí)行所有渲染操作翔怎,以避免將中間狀態(tài)呈現(xiàn)在屏幕上。一旦 Work Buffer 中的所有渲染操作完成备埃,React Fiber 將當(dāng)前緩存與工作緩存進(jìn)行切換姓惑,即將 Work Buffer 設(shè)置為當(dāng)前緩存,以此來更新屏幕上的 UI按脚。

這樣一來于毙,React Fiber 就可以確保在任何時候,所有呈現(xiàn)在屏幕上的內(nèi)容都是完整和穩(wěn)定的辅搬。

mount與update 場景

當(dāng)組件第一次被掛載時:

class MyComponent extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; } handleClick = () => { this.setState((prevState) => ({ count: prevState.count + 1, })); } render() { return ( <div onClick={this.handleClick}> Click me: {this.state.count} </div> ); }}ReactDOM.render(<MyComponent />, document.getElementById('root'));

當(dāng)我們將 <MyComponent /> 掛載到頁面上時唯沮,React Fiber 首先會在內(nèi)存中創(chuàng)建一個空的 Fiber 樹,然后根據(jù)組件的定義堪遂,為組件創(chuàng)建一個初始的“工作單元”(Work In Progress)介蛉。

在這個工作單元內(nèi)部,React Fiber 會為狀態(tài)和 props 建立初始的 Fiber 對象溶褪,并在之后的更新過程中使用這些 Fiber 對象來跟蹤組件的狀態(tài)和變化币旧。這樣可以確保任何時候都可以根據(jù)狀態(tài)和 props 的變化來更新 UI,而不會出現(xiàn)任何問題猿妈。

接下來吹菱,React Fiber 開始在工作單元中執(zhí)行所有的渲染操作,生成一棵虛擬 DOM 樹彭则,并將其添加到 Work Buffer 中鳍刷。然后,React Fiber 會檢查 Work Buffer 是否有更改俯抖,如果有更改输瓜,就將 Work Buffer 與 Current Buffer 進(jìn)行對比,以查找差異并更新到 DOM 上。

這個初次渲染的過程不太會涉及到雙緩存樹尤揣,因為當(dāng)前緩存是空的搔啊,所有的操作都是在 Work Buffer 中進(jìn)行的。但是芹缔,一旦初次渲染完成坯癣,并且組件狀態(tài)發(fā)生變化時,雙緩存樹就開始發(fā)揮作用了最欠。

當(dāng)我們通過點擊按鈕更新組件狀態(tài)時,React Fiber 將啟動一個新的渲染周期惩猫,并為更新創(chuàng)建一個新的工作單元芝硬。React Fiber 會在新的工作單元中更新狀態(tài)、生成新的虛擬 DOM 樹轧房,并將其添加到 Work Buffer 中拌阴。

然后,React Fiber 會將 Work Buffer 與 Current Buffer 進(jìn)行對比奶镶,找出差異并將其更新到 DOM 上迟赃。但是,由于雙緩存樹的存在厂镇,React Fiber 不會立即將 Work Buffer 切換到 Current Buffer纤壁,以避免將中間狀態(tài)顯示在屏幕上。

執(zhí)行流程

好的捺信,下面是 React Fiber 在頁面初次更新時的工作過程的流程圖:

  1. 應(yīng)用程序啟動酌媒,ReactDOM 調(diào)用 ReactDOM.render() 方法,并將組件渲染到 DOM 中迄靠,React Fiber 創(chuàng)建一個空的 Fiber 樹秒咨。
  2. React Fiber 為組件創(chuàng)建初始的“工作單元”,并在其中創(chuàng)建狀態(tài)和 props 的 Fiber 對象掌挚。
  3. React Fiber 執(zhí)行組件的 render() 方法雨席,生成虛擬 DOM 樹并添加到工作單元中。
  4. React Fiber 將工作單元中的虛擬 DOM 樹添加到 Work Buffer 中吠式。
  5. React Fiber 檢查 Work Buffer 是否有更改陡厘,如果有更改,則將其與 Current Buffer 進(jìn)行對比奇徒,并將差異更新到 DOM 上雏亚。
  6. 由于這是初次渲染,Current Buffer 為空摩钙,所有更新操作都在 Work Buffer 中完成罢低,然后將 Work Buffer 設(shè)置為 Current Buffer。
  7. React Fiber 在內(nèi)存中保留 Fiber 樹的副本,并用于后續(xù)的更新操作网持。此時宜岛,組件初次渲染流程結(jié)束。

323.[React] Fiber的含義與數(shù)據(jù)結(jié)構(gòu)【熱度: 1,778】【web框架】

關(guān)鍵詞:react16 架構(gòu)功舀、react Reconciler萍倡、react fiber、react 協(xié)調(diào)器

Fiber的含義

  1. 作為架構(gòu)來說辟汰,之前React15的Reconciler采用遞歸的方式執(zhí)行列敲,數(shù)據(jù)保存在遞歸調(diào)用棧中,所以被稱為stack Reconciler帖汞。React16的Reconciler基于Fiber節(jié)點實現(xiàn)戴而,被稱為Fiber Reconciler。

  2. 作為靜態(tài)的數(shù)據(jù)結(jié)構(gòu)來說翩蘸,每個Fiber節(jié)點對應(yīng)一個React element所意,保存了該組件的類型(函數(shù)組件/類組件/原生組件…)曹阔、對應(yīng)的DOM節(jié)點等信息啊送。

  3. 作為動態(tài)的工作單元來說,每個Fiber節(jié)點保存了本次更新中該組件改變的狀態(tài)实胸、要執(zhí)行的工作(需要被刪除/被插入頁面中/被更新…)郎任。

Fiber的結(jié)構(gòu)

總的屬性如下:

function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,) { // 作為靜態(tài)數(shù)據(jù)結(jié)構(gòu)的屬性 this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // 用于連接其他Fiber節(jié)點形成Fiber樹 this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; // 作為動態(tài)的工作單元的屬性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; // 調(diào)度優(yōu)先級相關(guān) this.lanes = NoLanes; this.childLanes = NoLanes; // 指向該fiber在另一次更新時對應(yīng)的fiber this.alternate = null;}

可以按三層含義將他們分類來看

作為架構(gòu)

每個Fiber節(jié)點有個對應(yīng)的React element秧耗,多個Fiber節(jié)點是如何連接形成樹呢?靠如下三個屬性:

// 指向父級Fiber節(jié)點this.return = null;// 指向子Fiber節(jié)點this.child = null;// 指向右邊第一個兄弟Fiber節(jié)點this.sibling = null;

舉個例子涝滴,如下的組件結(jié)構(gòu):

function App() { return ( <div> i am <span>KaSong</span> </div> )}

對應(yīng)的Fiber樹結(jié)構(gòu):

作為靜態(tài)的數(shù)據(jù)結(jié)構(gòu)

作為一種靜態(tài)的數(shù)據(jù)結(jié)構(gòu)绣版,保存了組件相關(guān)的信息:

// Fiber對應(yīng)組件的類型 Function/Class/Host...this.tag = tag;// key屬性this.key = key;// 大部分情況同type,某些情況不同歼疮,比如FunctionComponent使用React.memo包裹this.elementType = null;// 對于 FunctionComponent杂抽,指函數(shù)本身,對于ClassComponent韩脏,指class缩麸,對于HostComponent,指DOM節(jié)點tagNamethis.type = null;// Fiber對應(yīng)的真實DOM節(jié)點this.stateNode = null;

作為動態(tài)的工作單元

作為動態(tài)的工作單元赡矢,F(xiàn)iber中如下參數(shù)保存了本次更新相關(guān)的信息杭朱,我們會在后續(xù)的更新流程中使用到具體屬性時再詳細(xì)介紹

// 保存本次更新造成的狀態(tài)改變相關(guān)信息this.pendingProps = pendingProps;this.memoizedProps = null;this.updateQueue = null;this.memoizedState = null;this.dependencies = null;this.mode = mode;// 保存本次更新會造成的DOM操作this.effectTag = NoEffect;this.nextEffect = null;this.firstEffect = null;this.lastEffect = null;// 調(diào)度優(yōu)先級相關(guān)this.lanes = NoLanes;this.childLanes = NoLanes;

324.[React] render 階段的執(zhí)行過程【熱度: 1,793】【web框架】

關(guān)鍵詞:react16 架構(gòu)、react Reconciler吹散、react fiber弧械、react 協(xié)調(diào)器

render階段開始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的調(diào)用。這取決于本次更新是同步更新還是異步更新空民。

// performSyncWorkOnRoot會調(diào)用該方法function workLoopSync() { while (workInProgress !== null) { performUnitOfWork(workInProgress); }}// performConcurrentWorkOnRoot會調(diào)用該方法function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); }}

可以看到刃唐,他們唯一的區(qū)別是是否調(diào)用shouldYield羞迷。如果當(dāng)前瀏覽器幀沒有剩余時間,shouldYield會中止循環(huán)画饥,直到瀏覽器有空閑時間后再繼續(xù)遍歷衔瓮。

workInProgress代表當(dāng)前已創(chuàng)建的workInProgress fiber。

performUnitOfWork方法會創(chuàng)建下一個Fiber節(jié)點并賦值給workInProgress抖甘,并將workInProgress與已創(chuàng)建的Fiber節(jié)點連接起來構(gòu)成Fiber樹热鞍。

通過遍歷的方式實現(xiàn)可中斷的遞歸,所以performUnitOfWork的工作可以分為兩部分:“遞”和“歸”衔彻。

創(chuàng)建節(jié)點

首先從rootFiber開始向下深度優(yōu)先遍歷薇宠。為遍歷到的每個Fiber節(jié)點調(diào)用beginWork方法 (opens new window)。

該方法會根據(jù)傳入的Fiber節(jié)點創(chuàng)建子Fiber節(jié)點米奸,并將這兩個Fiber節(jié)點連接起來昼接。

當(dāng)遍歷到葉子節(jié)點(即沒有子組件的組件)時就會進(jìn)入“歸”階段。

在“歸”階段會調(diào)用completeWork (opens new window)處理Fiber節(jié)點悴晰。

當(dāng)某個Fiber節(jié)點執(zhí)行完completeWork,如果其存在兄弟Fiber節(jié)點(即fiber.sibling !== null)逐工,會進(jìn)入其兄弟Fiber的“遞”階段铡溪。

如果不存在兄弟Fiber,會進(jìn)入父級Fiber的“歸”階段泪喊。

“遞”和“歸”階段會交錯執(zhí)行直到“歸”到rootFiber棕硫。至此,render階段的工作就結(jié)束了袒啼。

舉例

代碼如下:

function App() { return ( <div> i am <span>KaSong</span> </div> )}ReactDOM.render(<App/>, document.getElementById("root"));

對應(yīng)的 fiber 樹結(jié)構(gòu)如下

render 階段會依次執(zhí)行

1. rootFiber beginWork2. App Fiber beginWork3. div Fiber beginWork4. "i am" Fiber beginWork5. "i am" Fiber completeWork6. span Fiber beginWork7. span Fiber completeWork8. div Fiber completeWork9. App Fiber completeWork10. rootFiber completeWork

beginWork

源碼鏈接: https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3075

工作流程圖:

beginWork的工作是傳入當(dāng)前Fiber節(jié)點哈扮,創(chuàng)建子Fiber節(jié)點,我們從傳參來看看具體是如何做的蚓再。

傳參

function beginWork( current: Fiber | null, // 當(dāng)前組件對應(yīng)的Fiber節(jié)點在上一次更新時的Fiber節(jié)點滑肉,即workInProgress.alternate workInProgress: Fiber, // 當(dāng)前組件對應(yīng)的Fiber節(jié)點 renderLanes: Lanes, // 優(yōu)先級相關(guān),在講解Scheduler時再講解): Fiber | null { // ...省略函數(shù)體}

beginWork的工作可以分為兩部分:

  • update時:如果current存在摘仅,在滿足一定條件時可以復(fù)用current節(jié)點靶庙,這樣就能克隆current.child作為workInProgress.child,而不需要新建workInProgress.child娃属。
  • mount時:除fiberRootNode以外六荒,current === null。會根據(jù)fiber.tag不同矾端,創(chuàng)建不同類型的子Fiber節(jié)點
function beginWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes): Fiber | null { // update時:如果current存在可能存在優(yōu)化路徑掏击,可以復(fù)用current(即上一次更新的Fiber節(jié)點) if (current !== null) { // ...省略 // 復(fù)用current return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; } // mount時:根據(jù)tag不同,創(chuàng)建不同的子Fiber節(jié)點 switch (workInProgress.tag) { case IndeterminateComponent: // ...省略 case LazyComponent: // ...省略 case FunctionComponent: // ...省略 case ClassComponent: // ...省略 case HostRoot: // ...省略 case HostComponent: // ...省略 case HostText: // ...省略 // ...省略其他類型 }}

update時

滿足如下情況時didReceiveUpdate === false(即可以直接復(fù)用前一次更新的子Fiber秩铆,不需要新建子Fiber)

if (current !== null) { const oldProps = current.memoizedProps; const newProps = workInProgress.pendingProps; if ( oldProps !== newProps || hasLegacyContextChanged() || (__DEV__ ? workInProgress.type !== current.type : false) ) { didReceiveUpdate = true; } else if (!includesSomeLane(renderLanes, updateLanes)) { didReceiveUpdate = false; switch (workInProgress.tag) { // 省略處理 } return bailoutOnAlreadyFinishedWork( current, workInProgress, renderLanes, ); } else { didReceiveUpdate = false; }} else { didReceiveUpdate = false;}
  1. oldProps === newProps && workInProgress.type === current.type砚亭,即props與fiber.type不變
  2. !includesSomeLane(renderLanes, updateLanes),即當(dāng)前Fiber節(jié)點優(yōu)先級不夠,會在講解Scheduler時介紹

mount

當(dāng)不滿足優(yōu)化路徑時钠惩,我們就進(jìn)入第二部分柒凉,新建子Fiber。

// mount時:根據(jù)tag不同篓跛,創(chuàng)建不同的Fiber節(jié)點switch (workInProgress.tag) { case IndeterminateComponent: // ...省略 case LazyComponent: // ...省略 case FunctionComponent: // ...省略 case ClassComponent: // ...省略 case HostRoot: // ...省略 case HostComponent: // ...省略 case HostText: // ...省略 // ...省略其他類型}

我們可以看到膝捞,根據(jù)fiber.tag不同,進(jìn)入不同類型Fiber的創(chuàng)建邏輯愧沟。

對于我們常見的組件類型蔬咬,如(FunctionComponent/ClassComponent/HostComponent),最終會進(jìn)入reconcileChildren (opens new window)方法沐寺。

reconcileChildren

  • 對于mount的組件林艘,他會創(chuàng)建新的子Fiber節(jié)點
  • 對于update的組件,他會將當(dāng)前組件與該組件在上次更新時對應(yīng)的Fiber節(jié)點比較(也就是俗稱的Diff算法)混坞,將比較的結(jié)果生成新Fiber節(jié)點
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes) { if (current === null) { // 對于mount的組件 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes, ); } else { // 對于update的組件 workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); }}

從代碼可以看出狐援,和beginWork一樣,他也是通過current === null ?區(qū)分mount與update究孕。

不論走哪個邏輯啥酱,最終他會生成新的子Fiber節(jié)點并賦值給workInProgress.child,作為本次beginWork返回值 (opens new window)厨诸,并作為下次performUnitOfWork執(zhí)行時workInProgress的傳參

effectTag

我們知道镶殷,render階段的工作是在內(nèi)存中進(jìn)行,當(dāng)工作結(jié)束后會通知Renderer需要執(zhí)行的DOM操作微酬。要執(zhí)行DOM操作的具體類型就保存在fiber.effectTag中绘趋。

// DOM需要插入到頁面中export const Placement = /* */ 0b00000000000010;// DOM需要更新export const Update = /* */ 0b00000000000100;// DOM需要插入到頁面中并更新export const PlacementAndUpdate = /* */ 0b00000000000110;// DOM需要刪除export const Deletion = /* */ 0b00000000001000;

通過二進(jìn)制表示effectTag,可以方便的使用位操作為fiber.effectTag賦值多個effect颗管。

那么陷遮,如果要通知Renderer將Fiber節(jié)點對應(yīng)的DOM節(jié)點插入頁面中,需要滿足兩個條件:

  1. fiber.stateNode存在忙上,即Fiber節(jié)點中保存了對應(yīng)的DOM節(jié)點

  2. (fiber.effectTag & Placement) !== 0拷呆,即 Fiber節(jié)點存在Placement effectTag

我們知道,mount時疫粥,fiber.stateNode === null茬斧,且在reconcileChildren中調(diào)用的mountChildFibers不會為Fiber節(jié)點賦值effectTag。那么首屏渲染如何完成呢梗逮?

針對第一個問題项秉,fiber.stateNode會在completeWork中創(chuàng)建,我們會在下一節(jié)介紹慷彤。

第二個問題的答案十分巧妙:假設(shè)mountChildFibers也會賦值effectTag娄蔼,那么可以預(yù)見mount時整棵Fiber樹所有節(jié)點都會有PlacementeffectTag怖喻。那么commit階段在執(zhí)行DOM操作時每個節(jié)點都會執(zhí)行一次插入操作,這樣大量的DOM操作是極低效的岁诉。

為了解決這個問題锚沸,在mount時只有rootFiber會賦值Placement effectTag,在commit階段只會執(zhí)行一次插入操作涕癣。

completeWork

流程圖:

類似beginWork哗蜈,completeWork也是針對不同fiber.tag調(diào)用不同的處理邏輯。

function completeWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes,): Fiber | null { const newProps = workInProgress.pendingProps; switch (workInProgress.tag) { case IndeterminateComponent: case LazyComponent: case SimpleMemoComponent: case FunctionComponent: case ForwardRef: case Fragment: case Mode: case Profiler: case ContextConsumer: case MemoComponent: return null; case ClassComponent: { // ...省略 return null; } case HostRoot: { // ...省略 updateHostContainer(workInProgress); return null; } case HostComponent: { // ...省略 return null; } // ...省略 } // ...省略}

我們重點關(guān)注頁面渲染所必須的 HostComponent(即原生DOM組件對應(yīng)的Fiber節(jié)點)坠韩,其他類型Fiber的處理留在具體功能實現(xiàn)時講解距潘。

處理 HostComponent

beginWork一樣,我們根據(jù) current === null ?判斷是mount還是update只搁。

同時針對 HostComponent音比,判斷 update 時我們還需要考慮 workInProgress.stateNode != null ?(即該Fiber節(jié)點是否存在對應(yīng)的DOM節(jié)點)

caseHostComponent: { popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); const type = workInProgress.type; if (current !== null && workInProgress.stateNode != null) { // update的情況 // ...省略 } else { // mount的情況 // ...省略 } return null;}

update 時

當(dāng)update時,F(xiàn)iber節(jié)點已經(jīng)存在對應(yīng)DOM節(jié)點氢惋,所以不需要生成DOM節(jié)點洞翩。需要做的主要是處理props,比如:

  • onClick焰望、onChange 等回調(diào)函數(shù)的注冊
  • 處理 style prop
  • 處理 DANGEROUSLY_SET_INNER_HTML prop
  • 處理 children prop

我們?nèi)サ粢恍┊?dāng)前不需要關(guān)注的功能(比如ref)菱农。可以看到最主要的邏輯是調(diào)用updateHostComponent方法柿估。

if (current !== null && workInProgress.stateNode != null) { // update的情況 updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, );}

在updateHostComponent內(nèi)部,被處理完的props會被賦值給workInProgress.updateQueue陷猫,并最終會在commit階段被渲染在頁面上秫舌。

workInProgress.updateQueue = (updatePayload: any);

其中updatePayload為數(shù)組形式,他的偶數(shù)索引的值為變化的prop key绣檬,奇數(shù)索引的值為變化的prop value足陨。

mount 時

同樣,我們省略了不相關(guān)的邏輯娇未∧担可以看到,mount時的主要邏輯包括三個:

  • 為Fiber節(jié)點生成對應(yīng)的DOM節(jié)點
  • 將子孫DOM節(jié)點插入剛生成的DOM節(jié)點中
  • 與update邏輯中的updateHostComponent類似的處理props的過程
// mount的情況// ...省略服務(wù)端渲染相關(guān)邏輯const currentHostContext = getHostContext();// 為fiber創(chuàng)建對應(yīng)DOM節(jié)點const instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, );// 將子孫DOM節(jié)點插入剛生成的DOM節(jié)點中appendAllChildren(instance, workInProgress, false, false);// DOM節(jié)點賦值給fiber.stateNodeworkInProgress.stateNode = instance;// 與update邏輯中的updateHostComponent類似的處理props的過程if ( finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, )) { markUpdate(workInProgress);}

mount時只會在rootFiber存在Placement effectTag零抬。那么commit階段是如何通過一次插入DOM操作(對應(yīng)一個Placement effectTag)將整棵DOM樹插入頁面的呢镊讼?

原因就在于 completeWork中的appendAllChildren 方法。

由于completeWork屬于“歸”階段調(diào)用的函數(shù)平夜,每次調(diào)用appendAllChildren時都會將已生成的子孫DOM節(jié)點插入當(dāng)前生成的DOM節(jié)點下蝶棋。那么當(dāng)“歸”到rootFiber時,我們已經(jīng)有一個構(gòu)建好的離屏DOM樹忽妒。

effectList

至此render階段的絕大部分工作就完成了玩裙。

還有一個問題:作為DOM操作的依據(jù)兼贸,commit階段需要找到所有有effectTag的Fiber節(jié)點并依次執(zhí)行effectTag對應(yīng)操作。難道需要在commit階段再遍歷一次Fiber樹尋找effectTag !== null的Fiber節(jié)點么吃溅?

這顯然是很低效的溶诞。

為了解決這個問題,在completeWork的上層函數(shù)completeUnitOfWork中决侈,每個執(zhí)行完completeWork且存在effectTag的Fiber節(jié)點會被保存在一條被稱為effectList的單向鏈表中螺垢。

effectList中第一個Fiber節(jié)點保存在fiber.firstEffect,最后一個元素保存在fiber.lastEffect颜及。

類似appendAllChildren甩苛,在“歸”階段,所有有effectTag的Fiber節(jié)點都會被追加在effectList中俏站,最終形成一條以rootFiber.firstEffect為起點的單向鏈表讯蒲。

                       nextEffect         nextEffectrootFiber.firstEffect -----------> fiber -----------> fiber

這樣,在commit階段只需要遍歷effectList就能執(zhí)行所有effect了肄扎。

流程結(jié)尾

至此墨林,render階段全部工作完成。在performSyncWorkOnRoot函數(shù)中fiberRootNode被傳遞給commitRoot方法犯祠,開啟commit階段工作流程旭等。

commitRoot(root);

資深開發(fā)者相關(guān)問題【共計 1 道題】

320.[React] React Reconciler 為何要采用 fiber 架構(gòu)?【熱度: 1,794】【web框架】

關(guān)鍵詞:react16 架構(gòu)衡载、react Reconciler搔耕、react fiber、react 協(xié)調(diào)器

代數(shù)效應(yīng)的實踐

React中做的就是踐行代數(shù)效應(yīng)(Algebraic Effects)痰娱。

簡單點兒來說就是: 用于將副作用從函數(shù)調(diào)用中分離弃榨。

舉例子:比如我們要獲取用戶的姓名做展示:

const resource = fetchProfileData();function ProfileDetails() { // Try to read user info, although it might not have loaded yet const user = resource.user.read(); return <h1>{user.name}</h1>;}

代碼如上, 但是 resource 是通過異步獲取的梨睁。 這個時候代碼就要改為下面這種形式

const resource = fetchProfileData();async function ProfileDetails() { // Try to read user info, although it might not have loaded yet const user = await resource.user.read(); return <h1>{user.name}</h1>;}

但是 async/await 是具有傳染性的鲸睛。 這個穿踐行就是副作用, 我們不希望有這樣的副作用坡贺, 盡管里面有異步調(diào)用官辈, 不希望這樣的副作用傳遞給外部的函數(shù), 只希望外部的函數(shù)是一個純函數(shù)遍坟。

代數(shù)效應(yīng)在React中的應(yīng)用

在 react 代碼中拳亿, 每一個函數(shù)式組件, 其實都是一個純函數(shù)政鼠, 但是內(nèi)部里面可能會有各種各樣的副作用风瘦。 這些副作用就是我們使用的 hooks;

對于類似useState、useReducer公般、useRef這樣的Hook万搔,我們不需要關(guān)注FunctionComponent的state在Hook中是如何保存的胡桨,React會為我們處理。

我們只需要假設(shè)useState返回的是我們想要的state瞬雹,并編寫業(yè)務(wù)邏輯就行昧谊。

可以看官方的 Suspense demo, 可以是通過 Suspense 讓內(nèi)部直接可以同步的方式調(diào)用異步代碼;代碼鏈接: https://codesandbox.io/s/frosty-hermann-bztrp?file=/src/index.js:152-160

import React, { Suspense } from "react";import ReactDOM from "react-dom";import "./styles.css";import { fetchProfileData } from "./fakeApi";const resource = fetchProfileData();function ProfilePage() { return ( <Suspense fallback={<h1>Loading profile...</h1>} > <ProfileDetails /> <Suspense fallback={<h1>Loading posts...</h1>} > <ProfileTimeline /> </Suspense> </Suspense> );}function ProfileDetails() { // Try to read user info, although it might not have loaded yet const user = resource.user.read(); return <h1>{user.name}</h1>;}function ProfileTimeline() { // Try to read posts, although they might not have loaded yet const posts = resource.posts.read(); return ( <ul> {posts.map(post => ( <li key={post.id}>{post.text}</li> ))} </ul> );}const rootElement = document.getElementById( "root");ReactDOM.createRoot(rootElement).render( <ProfilePage />);

Generator 架構(gòu)

從React15到React16酗捌,協(xié)調(diào)器(Reconciler)重構(gòu)的一大目的是:將老的同步更新的架構(gòu)變?yōu)楫惒娇芍袛喔隆?/p>

異步可中斷更新可以理解為:更新在執(zhí)行過程中可能會被打斷(瀏覽器時間分片用盡或有更高優(yōu)任務(wù)插隊)呢诬,當(dāng)可以繼續(xù)執(zhí)行時恢復(fù)之前執(zhí)行的中間狀態(tài)。

其實胖缤,瀏覽器原生就支持類似的實現(xiàn)尚镰,這就是Generator。

但是Generator的一些缺陷使React團(tuán)隊放棄了他:

  • 類似async哪廓,Generator也是傳染性的狗唉,使用了Generator則上下文的其他函數(shù)也需要作出改變。這樣心智負(fù)擔(dān)比較重涡真。
  • Generator執(zhí)行的中間狀態(tài)是上下文關(guān)聯(lián)的分俯。

例如這樣的例子:

function* doWork(A, B, C) { var x = doExpensiveWorkA(A); yield; var y = x + doExpensiveWorkB(B); yield; var z = y + doExpensiveWorkC(C); return z;}

但是當(dāng)我們考慮“高優(yōu)先級任務(wù)插隊”的情況,如果此時已經(jīng)完成doExpensiveWorkA與doExpensiveWorkB計算出x與y哆料。

此時B組件接收到一個高優(yōu)更新缸剪,由于Generator執(zhí)行的中間狀態(tài)是上下文關(guān)聯(lián)的,所以計算y時無法復(fù)用之前已經(jīng)計算出的x东亦,需要重新計算杏节。

如果通過全局變量保存之前執(zhí)行的中間狀態(tài),又會引入新的復(fù)雜度典阵。

fiber 架構(gòu)

他的中文翻譯叫做纖程拢锹,與進(jìn)程(Process)、線程(Thread)萄喳、協(xié)程(Coroutine)同為程序執(zhí)行過程。

在很多文章中將纖程理解為協(xié)程的一種實現(xiàn)蹋半。在JS中他巨,協(xié)程的實現(xiàn)便是Generator。

所以减江,我們可以將纖程(Fiber)染突、協(xié)程(Generator)理解為代數(shù)效應(yīng)思想在JS中的體現(xiàn)。

React Fiber可以理解為:

React內(nèi)部實現(xiàn)的一套狀態(tài)更新機(jī)制辈灼。支持任務(wù)不同優(yōu)先級份企,可中斷與恢復(fù),并且恢復(fù)后可以復(fù)用之前的中間狀態(tài)巡莹。

其中每個任務(wù)更新單元為React Element對應(yīng)的Fiber節(jié)點司志。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末甜紫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子骂远,更是在濱河造成了極大的恐慌囚霸,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件激才,死亡現(xiàn)場離奇詭異拓型,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瘸恼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門劣挫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人东帅,你說我怎么就攤上這事压固。” “怎么了冰啃?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵邓夕,是天一觀的道長。 經(jīng)常有香客問我阎毅,道長焚刚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任扇调,我火速辦了婚禮矿咕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狼钮。我一直安慰自己碳柱,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布熬芜。 她就那樣靜靜地躺著莲镣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涎拉。 梳的紋絲不亂的頭發(fā)上瑞侮,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音鼓拧,去河邊找鬼半火。 笑死,一個胖子當(dāng)著我的面吹牛季俩,可吹牛的內(nèi)容都是我干的钮糖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼酌住,長吁一口氣:“原來是場噩夢啊……” “哼店归!你這毒婦竟也來了阎抒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤娱节,失蹤者是張志新(化名)和其女友劉穎挠蛉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肄满,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡谴古,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了稠歉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掰担。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖怒炸,靈堂內(nèi)的尸體忽然破棺而出带饱,到底是詐尸還是另有隱情,我是刑警寧澤阅羹,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布勺疼,位于F島的核電站,受9級特大地震影響捏鱼,放射性物質(zhì)發(fā)生泄漏执庐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一导梆、第九天 我趴在偏房一處隱蔽的房頂上張望轨淌。 院中可真熱鬧,春花似錦看尼、人聲如沸递鹉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躏结。三九已至,卻和暖如春狰域,著一層夾襖步出監(jiān)牢的瞬間窜觉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工北专, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旬陡。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓拓颓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親描孟。 傳聞我的和親對象是個殘疾皇子驶睦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容