新聞中心
但如果被問:

- 有react fiber,為什么不需要 vue fiber呢;
- 之前遞歸遍歷虛擬dom樹被打斷就得從頭開始,為什么有了react fiber就能斷點(diǎn)恢復(fù)呢;
本文將從兩個(gè)框架的響應(yīng)式設(shè)計(jì)為切入口講清這兩個(gè)問題,不涉及晦澀源碼,不管有沒有使用過react,閱讀都不會有太大阻力。
什么是響應(yīng)式
無論你常用的是 react,還是 vue,“響應(yīng)式更新”這個(gè)詞肯定都不陌生。
響應(yīng)式,直觀來說就是視圖會自動(dòng)更新。如果一開始接觸前端就直接上手框架,會覺得這是理所當(dāng)然的,但在“響應(yīng)式框架”出世之前,實(shí)現(xiàn)這一功能是很麻煩的。
下面我將做一個(gè)時(shí)間顯示器,用原生 js、react、vue 分別實(shí)現(xiàn):
1.原生js:
想讓屏幕上內(nèi)容變化,必須需要先找到dom(document.getElementById),然后再修改dom(clockDom.innerText)。
有了響應(yīng)式框架,一切變得簡單了
2.react:
對內(nèi)容做修改,只需要調(diào)用setState去修改數(shù)據(jù),之后頁面便會重新渲染。
3.vue:
我們一樣不用關(guān)注dom,在修改數(shù)據(jù)時(shí),直接this.state=xxx修改,頁面就會展示最新的數(shù)據(jù)。
{{greet}}
現(xiàn)在是:{{time}}
react、vue的響應(yīng)式原理
上文提到修改數(shù)據(jù)時(shí),react需要調(diào)用setState方法,而vue直接修改變量就行??雌饋碇皇莾蓚€(gè)框架的用法不同罷了,但響應(yīng)式原理正在于此。
從底層實(shí)現(xiàn)來看修改數(shù)據(jù):在react中,組件的狀態(tài)是不能被修改的,setState沒有修改原來那塊內(nèi)存中的變量,而是去新開辟一塊內(nèi)存;
而vue則是直接修改保存狀態(tài)的那塊原始內(nèi)存。
所以經(jīng)常能看到react相關(guān)的文章里經(jīng)常會出現(xiàn)一個(gè)詞"immutable",翻譯過來就是不可變的。
數(shù)據(jù)修改了,接下來要解決視圖的更新:react中,調(diào)用setState方法后,會自頂向下重新渲染組件,自頂向下的含義是,該組件以及它的子組件全部需要渲染;而vue使用Object.defineProperty(vue@3遷移到了Proxy)對數(shù)據(jù)的設(shè)置(setter)和獲?。╣etter)做了劫持,也就是說,vue能準(zhǔn)確知道視圖模版中哪一塊用到了這個(gè)數(shù)據(jù),并且在這個(gè)數(shù)據(jù)修改時(shí),告訴這個(gè)視圖,你需要重新渲染了。
所以當(dāng)一個(gè)數(shù)據(jù)改變,react的組件渲染是很消耗性能的——父組件的狀態(tài)更新了,所有的子組件得跟著一起渲染,它不能像vue一樣,精確到當(dāng)前組件的粒度。
為了佐證,我分別用react和vue寫了一個(gè)demo,功能很簡單:父組件嵌套子組件,點(diǎn)擊父組件的按鈕會修改父組件的狀態(tài),點(diǎn)擊子組件的按鈕會修改子組件的狀態(tài)。
為了更好的對比,直觀展示渲染階段,沒用使用更流行的react函數(shù)式組件,vue也用的是不常見的render方法:
class Father extends React.Component{
state = {
fatherState:'Father-original state'
}
changeState = () => {
console.log('-----change Father state-----')
this.setState({fatherState:'Father-new state'})
}
render(){
console.log('Father:render')
return (
{this.state.fatherState}
)
}
}
class Child extends React.Component{
state = {
childState:'Child-original state'
}
changeState = () => {
console.log('-----change Child state-----')
this.setState({childState:'Child-new state'})
}
render(){
console.log('child:render')
return (
{this.state.childState}
)
}
}
ReactDOM.render( ,document.getElementById('root'))
上面是使用react時(shí)的效果,修改父組件的狀態(tài),父子組件都會重新渲染:點(diǎn)擊change Father state,不僅打印了Father:render,還打印了child:render。
const Father = Vue.createApp({
data() {
return {
fatherState:'Father-original state',
}
},
methods:{
changeState:function(){
console.log('-----change Father state-----')
this.fatherState = 'Father-new state'
}
},
render(){
console.log('Father:render')
return Vue.h('div',{},[
Vue.h('h2',this.fatherState),
Vue.h('button',{onClick:this.changeState},'change Father state'),
Vue.h('hr'),
Vue.h(Vue.resolveComponent('child'))
])
}
})
Father.component('child',{
data() {
return {
childState:'Child-original state'
}
},
methods:{
changeState:function(){
console.log('-----change Child state-----')
this.childState = 'Child-new state'
}
},
render(){
console.log('child:render')
return Vue.h('div',{},[
Vue.h('h3',this.childState),
Vue.h('button',{onClick:this.changeState},'change Child state'),
])
}
})
Father.mount('#root')
上面使用vue時(shí)的效果,無論是修改哪個(gè)狀態(tài),組件都只重新渲染最小顆粒:點(diǎn)擊change Father state,只打印Father:render,不會打印child:render。
不同響應(yīng)式原理的影響
首先需要強(qiáng)調(diào)的是,上文提到的“渲染”“render”“更新“都不是指瀏覽器真正渲染出視圖。而是框架在javascript層面上,調(diào)用自身實(shí)現(xiàn)的render方法,生成一個(gè)普通的對象,這個(gè)對象保存了真實(shí)dom的屬性,也就是常說的虛擬dom。本文會用組件渲染和頁面渲染對兩者做區(qū)分。
每次的視圖更新流程是這樣的:
- 組件渲染生成一棵新的虛擬dom樹;
- 新舊虛擬dom樹對比,找出變動(dòng)的部分;(也就是常說的diff算法)
- 為真正改變的部分創(chuàng)建真實(shí)dom,把他們掛載到文檔,實(shí)現(xiàn)頁面重渲染;
由于react和vue的響應(yīng)式實(shí)現(xiàn)原理不同,數(shù)據(jù)更新時(shí),第一步中react組件會渲染出一棵更大的虛擬dom樹。
fiber是什么
上面說了這么多,都是為了方便講清楚為什么需要react fiber:在數(shù)據(jù)更新時(shí),react生成了一棵更大的虛擬dom樹,給第二步的diff帶來了很大壓力——我們想找到真正變化的部分,這需要花費(fèi)更長的時(shí)間。js占據(jù)主線程去做比較,渲染線程便無法做其他工作,用戶的交互得不到響應(yīng),所以便出現(xiàn)了react fiber。
react fiber沒法讓比較的時(shí)間縮短,但它使得diff的過程被分成一小段一小段的,因?yàn)樗辛恕氨4婀ぷ鬟M(jìn)度”的能力。js會比較一部分虛擬dom,然后讓渡主線程,給瀏覽器去做其他工作,然后繼續(xù)比較,依次往復(fù),等到最后比較完成,一次性更新到視圖上。
fiber是一種新的數(shù)據(jù)結(jié)構(gòu)
上文提到了,react fiber使得diff階段有了被保存工作進(jìn)度的能力,這部分會講清楚為什么。
我們要找到前后狀態(tài)變化的部分,必須把所有節(jié)點(diǎn)遍歷。
在老的架構(gòu)中,節(jié)點(diǎn)以樹的形式被組織起來:每個(gè)節(jié)點(diǎn)上有多個(gè)指針指向子節(jié)點(diǎn)。要找到兩棵樹的變化部分,最容易想到的辦法就是深度優(yōu)先遍歷,規(guī)則如下:
- 從根節(jié)點(diǎn)開始,依次遍歷該節(jié)點(diǎn)的所有子節(jié)點(diǎn);
- 當(dāng)一個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)遍歷完成,才認(rèn)為該節(jié)點(diǎn)遍歷完成;
如果你系統(tǒng)學(xué)習(xí)過數(shù)據(jù)結(jié)構(gòu),應(yīng)該很快就能反應(yīng)過來,這不過是深度優(yōu)先遍歷的后續(xù)遍歷。根據(jù)這個(gè)規(guī)則,在圖中標(biāo)出了節(jié)點(diǎn)完成遍歷的順序。
這種遍歷有一個(gè)特點(diǎn),必須一次性完成。假設(shè)遍歷發(fā)生了中斷,雖然可以保留當(dāng)下進(jìn)行中節(jié)點(diǎn)的索引,下次繼續(xù)時(shí),我們的確可以繼續(xù)遍歷該節(jié)點(diǎn)下面的所有子節(jié)點(diǎn),但是沒有辦法找到其父節(jié)點(diǎn)——因?yàn)槊總€(gè)節(jié)點(diǎn)只有其子節(jié)點(diǎn)的指向。斷點(diǎn)沒有辦法恢復(fù),只能從頭再來一遍。
以該樹為例:
在遍歷到節(jié)點(diǎn)2時(shí)發(fā)生了中斷,我們保存對節(jié)點(diǎn)2的索引,下次恢復(fù)時(shí)可以把它下面的3、4節(jié)點(diǎn)遍歷到,但是卻無法找回5、6、7、8節(jié)點(diǎn)。
在新的架構(gòu)中,每個(gè)節(jié)點(diǎn)有三個(gè)指針:分別指向第一個(gè)子節(jié)點(diǎn)、下一個(gè)兄弟節(jié)點(diǎn)、父節(jié)點(diǎn)。這種數(shù)據(jù)結(jié)構(gòu)就是fiber,它的遍歷規(guī)則如下:
- 從根節(jié)點(diǎn)開始,依次遍歷該節(jié)點(diǎn)的子節(jié)點(diǎn)、兄弟節(jié)點(diǎn),如果兩者都遍歷了,則回到它的父節(jié)點(diǎn);
- 當(dāng)一個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)遍歷完成,才認(rèn)為該節(jié)點(diǎn)遍歷完成;
根據(jù)這個(gè)規(guī)則,同樣在圖中標(biāo)出了節(jié)點(diǎn)遍歷完成的順序。跟樹結(jié)構(gòu)對比會發(fā)現(xiàn),雖然數(shù)據(jù)結(jié)構(gòu)不同,但是節(jié)點(diǎn)的遍歷開始和完成順序一模一樣。不同的是,當(dāng)遍歷發(fā)生中斷時(shí),只要保留下當(dāng)前節(jié)點(diǎn)的索引,斷點(diǎn)是可以恢復(fù)的——因?yàn)槊總€(gè)節(jié)點(diǎn)都保持著對其父節(jié)點(diǎn)的索引。
同樣在遍歷到節(jié)點(diǎn)2時(shí)中斷,fiber結(jié)構(gòu)使得剩下的所有節(jié)點(diǎn)依舊能全部被走到。
這就是react fiber的渲染可以被中斷的原因。樹和fiber雖然看起來很像,但本質(zhì)上來說,一個(gè)是樹,一個(gè)是鏈表。
fiber是纖程
這種數(shù)據(jù)結(jié)構(gòu)之所以被叫做fiber,因?yàn)閒iber的翻譯是纖程,它被認(rèn)為是協(xié)程的一種實(shí)現(xiàn)形式。協(xié)程是比線程更小的調(diào)度單位:它的開啟、暫??梢员怀绦騿T所控制。具體來說,react fiber是通過requestIdleCallback這個(gè)api去控制的組件渲染的“進(jìn)度條”。
requesetIdleCallback是一個(gè)屬于宏任務(wù)的回調(diào),就像setTimeout一樣。不同的是,setTimeout的執(zhí)行時(shí)機(jī)由我們傳入的回調(diào)時(shí)間去控制,requesetIdleCallback是受屏幕的刷新率去控制。本文不對這部分做深入探討,只需要知道它每隔16ms會被調(diào)用一次,它的回調(diào)函數(shù)可以獲取本次可以執(zhí)行的時(shí)間,每一個(gè)16ms除了requesetIdleCallback的回調(diào)之外,還有其他工作,所以能使用的時(shí)間是不確定的,但只要時(shí)間到了,就會停下節(jié)點(diǎn)的遍歷。
使用方法如下:
const workLoop = (deadLine) => {
let shouldYield = false;// 是否該讓出線程
while(!shouldYield){
console.log('working')
// 遍歷節(jié)點(diǎn)等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
requestIdleCallback的回調(diào)函數(shù)可以通過傳入的參數(shù)deadLine.timeRemaining()檢查當(dāng)下還有多少時(shí)間供自己使用。上面的demo也是react fiber工作的偽代碼。
但由于兼容性不好,加上該回調(diào)函數(shù)被調(diào)用的頻率太低,react實(shí)際使用的是一個(gè)polyfill(自己實(shí)現(xiàn)的api),而不是requestIdleCallback。
現(xiàn)在,可以總結(jié)一下了:React Fiber是React 16提出的一種更新機(jī)制,使用鏈表取代了樹,將虛擬dom連接,使得組件更新的流程可以被中斷恢復(fù);它把組件渲染的工作分片,到時(shí)會主動(dòng)讓出渲染主線程。
react fiber帶來的變化
首先放一張?jiān)谏鐓^(qū)廣為流傳的對比圖,分別是用react 15和16實(shí)現(xiàn)的。這是一個(gè)寬度變化的三角形,每個(gè)小圓形中間的數(shù)字會隨時(shí)間改變,除此之外,將鼠標(biāo)懸停,小圓點(diǎn)的顏色會發(fā)生變化。
(戳這里是react15-stack在線地址|這里是react16-fiber)
實(shí)操一下,可以發(fā)現(xiàn)兩個(gè)特點(diǎn):
- 使用新架構(gòu)后,動(dòng)畫變得流暢,寬度的變化不會卡頓;
- 使用新架構(gòu)后,用戶響應(yīng)變快,鼠標(biāo)懸停時(shí)顏色變化更快;
看到到這里先稍微停一下,這兩點(diǎn)都是fiber帶給我們的嗎——用戶響應(yīng)變快是可以理解的,但使用react fiber能帶來渲染的加速嗎?
動(dòng)畫變流暢的根本原因,一定是一秒內(nèi)可以獲得更多動(dòng)畫幀。但是當(dāng)我們使用react fiber時(shí),并沒有減少更新所需要的總時(shí)間。
為了方便理解,我把刷新時(shí)的狀態(tài)做了一張圖:
上面是使用舊的react時(shí),獲得每一幀的時(shí)間點(diǎn),下面是使用fiber架構(gòu)時(shí),獲得每一幀的時(shí)間點(diǎn),因?yàn)榻M件渲染被分片,完成一幀更新的時(shí)間點(diǎn)反而被推后了,我們把一些時(shí)間片去處理用戶響應(yīng)了。
這里要注意,不會出現(xiàn)“一次組件渲染沒有完成,頁面部分渲染更新”的情況,react會保證每次更新都是完整的。
但頁面的動(dòng)畫確實(shí)變得流暢了,這是為什么呢?
我把該項(xiàng)目的 代碼倉庫 down下來,看了一下它的動(dòng)畫實(shí)現(xiàn):組件動(dòng)畫效果并不是直接修改width獲得的,而是使用的transform:scale屬性搭配3D變換。如果你聽說過硬件加速,大概知道為什么了:這樣設(shè)置頁面的重新渲染不依賴上圖中的渲染主線程,而是在GPU中直接完成。也就是說,這個(gè)渲染主線程線程只用保證有一些時(shí)間片去響應(yīng)用戶交互就可以了。
-
+
{this.state.seconds}
修改一下項(xiàng)目代碼中152行,把圖形的變化改為寬度width修改,會發(fā)現(xiàn)即使用react fiber,動(dòng)畫也會變得相當(dāng)卡頓,所以這里的流暢主要是CSS動(dòng)畫的功勞。(內(nèi)存不大的電腦謹(jǐn)慎嘗試,瀏覽器會卡死)
react不如vue?
我們現(xiàn)在已經(jīng)知道了react fiber是在彌補(bǔ)更新時(shí)“無腦”刷新,不夠精確帶來的缺陷。這是不是能說明react性能更差呢?
并不是。孰優(yōu)孰劣是一個(gè)很有爭議的話題,在此不做評價(jià)。因?yàn)関ue實(shí)現(xiàn)精準(zhǔn)更新也是有代價(jià)的,一方面是需要給每一個(gè)組件配置一個(gè)“監(jiān)視器”,管理著視圖的依賴收集和數(shù)據(jù)更新時(shí)的發(fā)布通知,這對性能同樣是有消耗的;另一方面vue能實(shí)現(xiàn)依賴收集得益于它的模版語法,實(shí)現(xiàn)靜態(tài)編譯,這是使用更靈活的JSX語法的react做不到的。
在react fiber出現(xiàn)之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法給我們,來聲明哪些是不需要連帶更新子組件。
結(jié)語
回到開頭的幾個(gè)問題,答案不難在文中找到:
- react因?yàn)橄忍斓牟蛔恪獰o法精確更新,所以需要react fiber把組件渲染工作切片;而vue基于數(shù)據(jù)劫持,更新粒度很小,沒有這個(gè)壓力;
- react fiber這種數(shù)據(jù)結(jié)構(gòu)使得節(jié)點(diǎn)可以回溯到其父節(jié)點(diǎn),只要保留下中斷的節(jié)點(diǎn)索引,就可以恢復(fù)之前的工作進(jìn)度。
名稱欄目:阿里三面:有Reactfiber,為什么不需要Vuefiber呢?
網(wǎng)頁URL:http://m.fisionsoft.com.cn/article/djesheg.html


咨詢
建站咨詢
