React源碼剖析——(四)新引擎React Fiber

????????在《JavaScript異步機制》這篇文章中我們說到晌姚,Js引擎是單線程的,它負(fù)責(zé)維護任務(wù)棧,并通過 Event Loop 的機制萎羔,按順序把任務(wù)放入棧中執(zhí)行液走,React的底層也是javaScript,因此他也不置可否的必須按照js的規(guī)則執(zhí)行。那么React的優(yōu)勢在哪呢缘眶?我們接著往下看嘱根。

????????稍有經(jīng)驗的前端工程師會知道,頁面的DOM改變巷懈,就會導(dǎo)致頁面重新計算DOM该抒,進行重繪或者重排,DOM結(jié)構(gòu)復(fù)雜或者頻繁操作DOM通常是性能瓶頸產(chǎn)生的原因顶燕。而網(wǎng)站從最開始比較簡單凑保,開始變的越來越復(fù)雜,用戶交互也會越來越多涌攻,怎么去減輕DOM操作帶來的性能損耗就變得重要起來欧引。這時候,我們的React帶著強大的創(chuàng)造性恳谎,順應(yīng)時代而生芝此,Virtual DOM的提出引領(lǐng)了前端的變革,Virtual DOM 是一個 JavaScript 對象因痛。每次婚苹,我們只需要告訴 React 下一個狀態(tài)是什么,React會自己構(gòu)建一個新的 Virtual DOM鸵膏,然后根據(jù)又一史詩性的創(chuàng)造--React diff算法(之前也著重剖析過React diff的源碼膊升,傳送門)快速計算其差異,找出需要重繪或重排的元素较性,告訴瀏覽器用僧。瀏覽器根據(jù)相關(guān)的更新,重新計算 DOM Tree赞咙,重繪頁面责循。我們來看一個例子:

1

這個例子會在頁面中創(chuàng)建一個輸入框,一個按鈕攀操,一個 BlockList 組件院仿。BlockList 組件會根據(jù) NUMBER_OF_BLOCK 的數(shù)值渲染出對應(yīng)數(shù)量的數(shù)字顯示框,數(shù)字顯示框顯示點擊按鈕的次數(shù)速和。我們最開始設(shè)置 據(jù) NUMBER_OF_BLOCK 為 2 歹垫,只渲染 2 個數(shù)字顯示框。

首次渲染出頁面之后颠放,我們點擊按鈕排惨,頁面中的數(shù)字顯示框的值由 0 變?yōu)?1。如下圖所示:

2

當(dāng)點擊按鈕的時候碰凶,按鈕點擊次數(shù)從 0 變?yōu)?1暮芭,我們需要告訴 React 下面你要顯示 1 了鹿驼,于是,通過 setState 操作辕宏,我們告訴 React: 下一個你需要顯示的數(shù)據(jù)是 1畜晰。然后,React 開始更新組件瑞筐。對應(yīng)的 Virtual DOM Tree 變化如下圖所示凄鼻。黃色表示狀態(tài)被更新。

3

我們點擊按鈕聚假,觸發(fā) setState 之后块蚌,React 就會創(chuàng)建一個新的 Virtual DOM,然后將新舊 Virtual DOM 進行 diff 操作魔策,判斷哪些元素需要更新匈子,將需要更新的元素放到更新列表中,最后遍歷更新列表更新所有的元素闯袒,這所有的過程都是 React 幫我們完成的虎敦。對瀏覽器而言,這個過程僅僅是編譯執(zhí)行了一段 JavaScript 代碼而已政敢,我們把從 setState 開始其徙,到頁面渲染結(jié)束的瀏覽器主線程工作流程畫出來,如下圖所示喷户。藍(lán)色粗線表示瀏覽器主線程唾那。

4

從獲得最新的數(shù)據(jù),到將數(shù)據(jù)在頁面中呈現(xiàn)出來褪尝,可以分為兩個階段闹获。

第一個 調(diào)度階段。這個階段 React 用新數(shù)據(jù)生成新的 Virtual DOM 河哑,遍歷 Virtual DOM 避诽,然后通過 Diff 算法,快速找出需要更新的元素璃谨,放到更新隊列中去沙庐。

第二個 渲染階段。這個階段 React 根據(jù)所在的渲染環(huán)境佳吞,遍歷更新隊列拱雏,將對應(yīng)元素更新。在瀏覽器中底扳,就是跟新對應(yīng)的DOM元素铸抑。除瀏覽器外,渲染環(huán)境還可以是 Native衷模,硬件鹊汛,VR 等菇爪。

新問題

之前,React 在官網(wǎng)中寫道:

We built React to solve one problem: building large applications with data that changes over time.

現(xiàn)在更新為:

React is a declarative, efficient, and flexible JavaScript library for building user interfaces.

所以我們看出柒昏,React 新的定位在于靈活高效的數(shù)據(jù)。但是在實際的使用中熙揍,尤其是遇到頁面結(jié)構(gòu)復(fù)雜职祷,數(shù)據(jù)更新頻繁的應(yīng)用的時候,React 的表現(xiàn)不盡如人意届囚。在上一個例子中有梆,我們可以設(shè)置 NUMBER_OF_BLOCK 的值為 100000(實際情況下,可能沒有那么多)意系,將其變?yōu)橐粋€“復(fù)雜”的網(wǎng)頁泥耀。

點擊按鈕,觸發(fā) setState蛔添,頁面開始更新痰催。

點擊輸入框,輸入一些字符串迎瞧,比如 “hireact”夸溶。我們可以看到,頁面此時沒有任何的響應(yīng)凶硅。

等待 7 s缝裁,輸入框中突然出現(xiàn)了,之前輸入的 “hireact”足绅,同時捷绑, BlockList 組件也更新了。

在這等待 7 s 中氢妈,頁面不會給我任何的響應(yīng)粹污,我會以為網(wǎng)站崩潰了,或者電腦死機了允懂。如果沒有讓我等待幾秒厕怜,只是等待了0.5秒,多等待幾個0.5秒之后我會在心里默想:這是什么破網(wǎng)站蕾总!

顯而易見粥航,這樣的用戶體驗并不好。

將瀏覽器主線程在這 7 s 的 performance 如下圖所示:


5

Fiber

可以確定的是復(fù)雜度為常數(shù)的 diff 算法還是很優(yōu)秀的生百,主要問題出現(xiàn)在递雀,React 的調(diào)度策略 -- Stack Reconfile。這個策略像函數(shù)調(diào)用棧一樣蚀浆,會深度優(yōu)先遍歷所有的 Virtual DOM 節(jié)點缀程,進行Diff搜吧。它一定要等整棵 Virtual DOM 計算完成之后,才將任務(wù)出棧釋放主線程杨凑。重點在于滤奈,Stack Reconfile始終會一次性地同步處理整個組件樹。Stack Reconciler無法暫停撩满,因此如果更新較為深入并且可用CPU時間有限蜒程,這種做法并非最優(yōu)化的。所以伺帘,在瀏覽器主線程被 React更新狀態(tài)任務(wù)占據(jù)的時候昭躺,用戶與瀏覽器進行任何的交互都不能得到反饋,只有等到任務(wù)結(jié)束伪嫁,才能突然得到瀏覽器的響應(yīng)领炫。

React 這樣的調(diào)度策略對動畫的支持也不好。如果 React 更新一次狀態(tài)张咳,占用瀏覽器主線程的時間超過 16.6 ms帝洪,就會被人眼發(fā)現(xiàn)前后兩幀不連續(xù),給用戶呈現(xiàn)出動畫卡頓的效果脚猾。

React 核心團隊很早之前就預(yù)知這樣的風(fēng)險的存在碟狞,并且持續(xù)探索可解決的方式』榕悖基于瀏覽器對requestIdleCallbackrequestAnimationFrame這兩個API 的支持族沃,以及其他團隊對者兩個API的實現(xiàn),如 React Native 團隊泌参。React 團隊實現(xiàn)新的調(diào)度策略 -- Fiber reconcile脆淹。

Fiber是一種輕量的執(zhí)行線程,同線程一樣共享定址空間沽一,線程靠系統(tǒng)調(diào)度盖溺,并且是搶占式多任務(wù)處理,F(xiàn)iber 則是自調(diào)用铣缠,協(xié)作式多任務(wù)處理烘嘱。

Fiber Reconcile 與 Stack Reconcile 主要有兩方面的不同。

首先蝗蛙,使用協(xié)作式多任務(wù)處理任務(wù)蝇庭。將原來的整個 Virtual DOM 的更新任務(wù)拆分成一個個小的任務(wù)。每次做完一個小任務(wù)之后捡硅,放棄一下自己的執(zhí)行將主線程空閑出來哮内,看看有沒有其他的任務(wù)。如果有的話壮韭,就暫停本次任務(wù)北发,執(zhí)行其他的任務(wù)纹因,如果沒有的話,就繼續(xù)下一個任務(wù)琳拨。

整個頁面更新并重渲染過程分為兩個階段瞭恰。

????1、Reconcile 階段狱庇。此階段中寄疏,依序遍歷組件,通過diff 算法僵井,判斷組件是否需要更新,給需要更新的組件加上tag驳棱。遍歷完之后批什,將所有帶有tag的組件加到一個數(shù)組中。這個階段的任務(wù)可以被打斷社搅。

????2驻债、Commit 階段。根據(jù)在 Reconcile 階段生成的數(shù)組形葬,遍歷更新DOM合呐,這個階段需要一次性執(zhí)行完。如果是在其他的渲染環(huán)境 -- Native笙以,硬件淌实,就會更新對應(yīng)的元素。

所以之前瀏覽器主線程執(zhí)行更新任務(wù)的執(zhí)行流程就變成了這樣:

Fiber1

其次猖腕,對任務(wù)進行優(yōu)先級劃分拆祈。不是每來一個新任務(wù),就要放棄現(xiàn)執(zhí)行任務(wù)倘感,轉(zhuǎn)而執(zhí)行新任務(wù)放坏。與我們做事情一樣,將任務(wù)劃分優(yōu)先級老玛,只有當(dāng)比現(xiàn)任務(wù)優(yōu)先級高的任務(wù)來了淤年,才需要放棄現(xiàn)任務(wù)的執(zhí)行。比如說蜡豹,屏幕外元素的渲染和更新任務(wù)的優(yōu)先級應(yīng)該小于響應(yīng)用戶輸入任務(wù)麸粮。若現(xiàn)在進行屏幕外組件狀態(tài)更新,用戶又在輸入镜廉,瀏覽器就應(yīng)該先執(zhí)行響應(yīng)用戶輸入任務(wù)豹休。瀏覽器主線程任務(wù)執(zhí)行流程如下圖所示:


Fiber2

我們重寫一個組件,跟之前的一樣桨吊。一個輸入框威根,一個按鈕凤巨,一個 BlockList 組件。BlockList 組件會根據(jù)NUMBER_OF_BLOCK 的數(shù)值渲染出對應(yīng)數(shù)量的數(shù)字顯示框洛搀,數(shù)字顯示框顯示點擊按鈕的次數(shù)敢茁。將 NUMBER_OF_BLOCK 設(shè)置為 100000,模擬一個復(fù)雜的頁面留美。不同的是彰檬,使用 Fiber reconcile 調(diào)度策略,設(shè)置任務(wù)優(yōu)先級谎砾,讓瀏覽器先響應(yīng)用戶輸入再執(zhí)行組件更新逢倍。

5

在對比代碼差異之前,我們先執(zhí)行同樣的操作景图,對比一下瀏覽器的行為较雕。

點擊 button,觸發(fā) setState挚币,頁面開始更新亮蒋。

點擊輸入框,輸入一些字符串妆毕,比如 “hireact”慎玖。我們可以看到,頁面能夠響應(yīng)我們的輸入了笛粘。

瀏覽器主線程的 performance 如下圖所示:

5

可以看到趁怔,在黃色 JavaScript 執(zhí)行過程中,也就是 React 占用瀏覽器主線程期間薪前,瀏覽器在也在重新計算 DOM Tree痕钢,并且進行重繪,截圖顯示序六,瀏覽器渲染的就是用戶新輸入的內(nèi)容任连。簡單說,在 React 占用瀏覽器主線程期間例诀,瀏覽器也在與用戶交互随抠。這個才是我們在網(wǎng)站上面期望獲得的體驗,瀏覽器總是對我的輸入有反饋繁涂。

那我們的代碼改變了哪些呢拱她?從下往上看:

首先,從 reactDOM.render() 變成了 ReactDOMFiber.render()扔罪。我們使用了 ReactFiber 去渲染整個頁面秉沼,ReactFiber 會將整個更新任務(wù)分成若干個小的更新任務(wù),然后設(shè)置一些任務(wù)默認(rèn)的優(yōu)先級。每執(zhí)行完一個小任務(wù)之后唬复,會釋放主線程矗积。

其次,render 方法中返回的不再是一個被 div 元素包一層的組件列表敞咧,而是直接返回一個組件列表棘捣,這是 React 在新版中提供的新的寫法。除此之外休建,可以直接返回字符串和數(shù)字乍恐。像下面:

render() {

????return 'Hi, ReactFiber!'

}

render() {

????????return 123

}

再次,我們傳給 setState 的不是最新狀態(tài)测砂,而是一個 callback茵烈,這個 callback 返回最新狀態(tài)。同上砌些,這個也是 React 新版中提供的新的寫法呜投,同時也是推薦的寫法。

最后寄症,我們沒有直接調(diào)用 setState,而是將其作為 callback 傳給了 unstable_deferredUpdates 這個 API矩动。從名字就可以看出有巧,deferredUpdates 是將更新推遲,unstable 表明現(xiàn)在還不穩(wěn)定悲没,在開發(fā)階段篮迎。從源代碼上看,unstable_deferredUpdates 做了一件事情示姿,就是將傳給它的更新任務(wù)的優(yōu)先級設(shè)置為 lowpriority甜橱。所以我們將seState 作為 callback 傳給了 unstable_deferredUpdates,就是告訴 React栈戳,這個 setState 任務(wù)岂傲,是一個 lowpriority 的任務(wù)。(需要注意的是子檀,并不確定 React 團隊是否將 unstable_deferredUpdates 或者deferredUpdates 作為一個開放的接口镊掖,現(xiàn)在這個版本[2]可以通過這個API去設(shè)置優(yōu)先級。同時褂痰,從源代碼可以看到亩进,React 團隊想要實現(xiàn)給任務(wù)設(shè)置優(yōu)先級的功能,目前只看到一個 performWithPriority 的接口缩歪,也還沒有實現(xiàn)归薛。)

我們點擊按鈕之后浴井,unstable_deferredUpdates 將這個更新任務(wù)設(shè)置為 low priority需纳。此時是沒有其他任務(wù)存在的,React 就開始進行狀態(tài)更新了。更新任務(wù)進入了 Reconcile 階段剥啤,我們點擊輸入框,此時点骑,用戶交互任務(wù)來了匾委,此任務(wù)優(yōu)先級高于更新任務(wù),所以瀏覽器主線程將焦點放在了輸入框……诅炉。之后更新任務(wù)進入了 Commit 階段蜡歹,不能將瀏覽器主線程放棄,到了最后瀏覽器渲染完成之后涕烧,將用戶在更新任務(wù) Commit 階段的輸入以及最新的狀態(tài)顯示出來月而。

對比 Stack Reconcile 和 Fiber Reconfile 的實現(xiàn),我們可以看到 React 新的調(diào)度策略讓開發(fā)者對 React 應(yīng)用有了更細(xì)節(jié)的控制议纯。開發(fā)者父款,可以通過優(yōu)先級,控制不同類型任務(wù)的優(yōu)先級瞻凤,提高用戶體驗憨攒,以及整個應(yīng)用程序的靈活性。

總結(jié)起來 ,Fiber Reconfile有著以下特性:

? ? 1阀参、能夠?qū)⒖芍袛嗟娜蝿?wù)拆分成塊肝集。

? ? 2、能夠?qū)M程中的工作劃分優(yōu)先級蛛壳、重新設(shè)定基址(Rebase)杏瞻、恢復(fù)。

? ? 3衙荐、能夠在父子之間來回反復(fù)捞挥,借此為React的Layout提供支持。

? ? 4忧吟、能夠通過render()返回多個元素砌函。

? ? 5、為錯誤邊界提供了更好的支持溜族。

簡單來說胸嘴,此時不在需要等待變更傳播到整個組件樹,React Fiber可以知道如何時不時暫停一下斩祭,檢查是否有其他更重要的更新劣像。這種調(diào)度能力主要基于requestIdleCallback的使用,而這是一種W3C候選推薦標(biāo)準(zhǔn)摧玫。

寫在最后

看起來 React Fiber 很厲害的樣子耳奕,如果要用的話绑青,還是有一些問題是需要考慮的。

比如說屋群,task 按照優(yōu)先級之后闸婴,可能低優(yōu)先級的任務(wù)永遠(yuǎn)不會執(zhí)行,稱之為 starvation芍躏;

比如說邪乍,task 有可能被打斷,需要重新執(zhí)行对竣,那么某些依賴生命周期實現(xiàn)的業(yè)務(wù)邏輯可能會受到影響庇楞。

……

React Fiber 也是帶來了很多的好處的。

比如說否纬,增強了某些領(lǐng)域的支持吕晌,比如動畫、布局和手勢临燃;

比如說睛驳,在復(fù)雜頁面,對用戶的反饋會更及時膜廊,應(yīng)用的用戶體驗會變好乏沸,簡單頁面看不到明顯的差異;

比如說爪瓜,api基本上沒有變化蹬跃,對現(xiàn)有項目很友好。

……

現(xiàn)在钥勋,React Fiber 已經(jīng)通過了所有的測試炬转,在網(wǎng)站Is Fiber Ready Yet?,上顯示辆苔,還有4個warning需要fix算灸。它會隨著 React 16 發(fā)布,到底效果怎么樣驻啤,只有用過才知道了菲驴。

參考資料:

Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

React Fiber Architecture

How React Fiber can make your web and mobile apps smoother and more responsive

How Browsers Work: Behind the scenes of modern web browsers

Tutorial: Intro To React

*本文中 React 的版本是 React@16.0.0-alpha.3

本文所有的例子和圖均來自劉杰鳳老師的文章《React的新引擎—React Fiber是什么?》骑冗,感謝~

至此赊瞬,React源碼剖析剖析的部分就寫完了,下面準(zhǔn)備寫一寫React技術(shù)棧的另一個偉大的部分——Redux,請大家期待(~ ̄▽ ̄)~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贼涩,一起剝皮案震驚了整個濱河市巧涧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遥倦,老刑警劉巖谤绳,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡缩筛,警方通過查閱死者的電腦和手機消略,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞎抛,“玉大人艺演,你說我怎么就攤上這事⊥╇” “怎么了胎撤?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長豪硅。 經(jīng)常有香客問我哩照,道長,這世上最難降的妖魔是什么懒浮? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任飘弧,我火速辦了婚禮,結(jié)果婚禮上砚著,老公的妹妹穿的比我還像新娘次伶。我一直安慰自己,他們只是感情好稽穆,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布冠王。 她就那樣靜靜地躺著,像睡著了一般舌镶。 火紅的嫁衣襯著肌膚如雪柱彻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天餐胀,我揣著相機與錄音哟楷,去河邊找鬼。 笑死否灾,一個胖子當(dāng)著我的面吹牛卖擅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播墨技,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惩阶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了扣汪?” 一聲冷哼從身側(cè)響起断楷,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎崭别,沒想到半個月后冬筒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體统刮,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年账千,在試婚紗的時候發(fā)現(xiàn)自己被綠了侥蒙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡匀奏,死狀恐怖鞭衩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情娃善,我是刑警寧澤论衍,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站聚磺,受9級特大地震影響坯台,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瘫寝,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一蜒蕾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧焕阿,春花似錦咪啡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至褒纲,卻和暖如春准夷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背莺掠。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工衫嵌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人汁蝶。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓渐扮,卻偏偏與公主長得像论悴,于是被迫代替她去往敵國和親掖棉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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

  • 前言 瀏覽器中的渲染引擎是單線程的膀估,幾乎所有的操作都在這個單線程中執(zhí)行——解析渲染DOM Tree和CSS Tre...
    ThoughtWorks閱讀 7,254評論 3 31
  • 參考文章:深度剖析:如何實現(xiàn)一個Virtual DOM 算法 作者:戴嘉華React中一個沒人能解釋清楚的問題——...
    waka閱讀 5,951評論 0 21
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南幔亥,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出察纯,比目前大...
    leonaxiong閱讀 2,810評論 1 18
  • 30年了帕棉,你老了鼓錘還在你從舊鼓皮中把時間撿起縫縫補補针肥,敲敲打打仔細(xì)端詳,像是老照片里的初戀 黃河水已干香伴,黃土風(fēng)沙...
    mao眼閱讀 366評論 0 1
  • 只是因為太年輕慰枕,所以所有的悲傷和快樂都顯得那么深刻,輕輕一碰就驚天動地即纲。
    毛線毛球閱讀 151評論 0 0