????????在《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赞咙,重繪頁面责循。我們來看一個例子:
這個例子會在頁面中創(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。如下圖所示:
當(dāng)點擊按鈕的時候碰凶,按鈕點擊次數(shù)從 0 變?yōu)?1暮芭,我們需要告訴 React 下面你要顯示 1 了鹿驼,于是,通過 setState 操作辕宏,我們告訴 React: 下一個你需要顯示的數(shù)據(jù)是 1畜晰。然后,React 開始更新組件瑞筐。對應(yīng)的 Virtual DOM Tree 變化如下圖所示凄鼻。黃色表示狀態(tài)被更新。
我們點擊按鈕聚假,觸發(fā) setState 之后块蚌,React 就會創(chuàng)建一個新的 Virtual DOM,然后將新舊 Virtual DOM 進行 diff 操作魔策,判斷哪些元素需要更新匈子,將需要更新的元素放到更新列表中,最后遍歷更新列表更新所有的元素闯袒,這所有的過程都是 React 幫我們完成的虎敦。對瀏覽器而言,這個過程僅僅是編譯執(zhí)行了一段 JavaScript 代碼而已政敢,我們把從 setState 開始其徙,到頁面渲染結(jié)束的瀏覽器主線程工作流程畫出來,如下圖所示喷户。藍(lán)色粗線表示瀏覽器主線程唾那。
從獲得最新的數(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 如下圖所示:
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ù)探索可解決的方式』榕悖基于瀏覽器對requestIdleCallback和requestAnimationFrame這兩個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í)行流程就變成了這樣:
其次猖腕,對任務(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í)行流程如下圖所示:
我們重寫一個組件,跟之前的一樣桨吊。一個輸入框威根,一個按鈕凤巨,一個 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í)行組件更新逢倍。
在對比代碼差異之前,我們先執(zhí)行同樣的操作景图,對比一下瀏覽器的行為较雕。
點擊 button,觸發(fā) setState挚币,頁面開始更新亮蒋。
點擊輸入框,輸入一些字符串妆毕,比如 “hireact”慎玖。我們可以看到,頁面能夠響應(yīng)我們的輸入了笛粘。
瀏覽器主線程的 performance 如下圖所示:
可以看到趁怔,在黃色 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
How React Fiber can make your web and mobile apps smoother and more responsive
How Browsers Work: Behind the scenes of modern web browsers
*本文中 React 的版本是 React@16.0.0-alpha.3
本文所有的例子和圖均來自劉杰鳳老師的文章《React的新引擎—React Fiber是什么?》骑冗,感謝~
至此赊瞬,React源碼剖析剖析的部分就寫完了,下面準(zhǔn)備寫一寫React技術(shù)棧的另一個偉大的部分——Redux,請大家期待(~ ̄▽ ̄)~