提到react fiber存崖,大部分人都知道這是一個react新特性冻记,看過一些網(wǎng)上的文章,大概能說出“纖程”“一種新的數(shù)據(jù)結(jié)構(gòu)”“更新時調(diào)度機制”等關(guān)鍵詞金句。
但如果被問:
- 有react fiber檩赢,為什么不需要 vue fiber呢;
- 之前遞歸遍歷虛擬dom樹被打斷就得從頭開始,為什么有了react fiber就能斷點恢復(fù)呢贞瞒;
本文將從兩個框架的響應(yīng)式設(shè)計為切入口講清這兩個問題偶房,不涉及晦澀源碼,不管有沒有使用過react军浆,閱讀都不會有太大阻力棕洋。
什么是響應(yīng)式
無論你常用的是 react,還是 vue乒融,“響應(yīng)式更新”這個詞肯定都不陌生掰盘。
響應(yīng)式,直觀來說就是視圖會自動更新赞季。如果一開始接觸前端就直接上手框架愧捕,會覺得這是理所當(dāng)然的,但在“響應(yīng)式框架”出世之前申钩,實現(xiàn)這一功能是很麻煩的次绘。
下面我將做一個時間顯示器,用原生 js撒遣、react邮偎、vue 分別實現(xiàn):
- 原生js:
想讓屏幕上內(nèi)容變化,必須需要先找到dom(document.getElementById
),然后再修改dom(clockDom.innerText
)义黎。
<div id="root">
<div id="greet"></div>
<div id="clock"></div>
</div>
<script>
const clockDom = document.getElementById('clock');
const greetDom = document.getElementById('greet');
setInterval(() => {
clockDom.innerText = `現(xiàn)在是:${Util.getTime()}`
greetDom.innerText = Util.getGreet()
}, 1000);
</script>
有了響應(yīng)式框架禾进,一切變得簡單了
- react:
對內(nèi)容做修改,只需要調(diào)用setState
去修改數(shù)據(jù)廉涕,之后頁面便會重新渲染泻云。
<body>
<div id="root"></div>
<script type="text/babel">
function Clock() {
const [time, setTime] = React.useState()
const [greet, setGreet] = React.useState()
setInterval(() => {
setTime(Util.getTime())
setGreet(Util.getGreet())
}, 1000);
return (
<div>
<div>{greet}</div>
<div>現(xiàn)在是:{time}</div>
</div>
)
}
ReactDOM.render(<Clock/>,document.getElementById('root'))
</script>
</body>
- vue:
我們一樣不用關(guān)注dom,在修改數(shù)據(jù)時,直接this.state=xxx
修改火的,頁面就會展示最新的數(shù)據(jù)壶愤。
<body>
<div id="root">
<div>{{greet}}</div>
<div>現(xiàn)在是:{{time}}</div>
</div>
<script>
const Clock = Vue.createApp({
data(){
return{
time:'',
greet:''
}
},
mounted(){
setInterval(() => {
this.time = Util.getTime();
this.greet = Util.getGreet();
}, 1000);
}
})
Clock.mount('#root')
</script>
</body>
react、vue的響應(yīng)式原理
上文提到修改數(shù)據(jù)時馏鹤,react需要調(diào)用setState
方法征椒,而vue直接修改變量就行∨壤郏看起來只是兩個框架的用法不同罷了勃救,但響應(yīng)式原理正在于此。
從底層實現(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)一個詞"immutable",翻譯過來就是不可變的。
數(shù)據(jù)修改了瓢省,接下來要解決視圖的更新:react中弄息,調(diào)用setState
方法后,會自頂向下重新渲染組件勤婚,自頂向下的含義是摹量,該組件以及它的子組件全部需要渲染;而vue使用Object.defineProperty
(vue@3遷移到了Proxy)對數(shù)據(jù)的設(shè)置(setter
)和獲嚷ā(getter
)做了劫持缨称,也就是說,vue能準(zhǔn)確知道視圖模版中哪一塊用到了這個數(shù)據(jù)祝迂,并且在這個數(shù)據(jù)修改時睦尽,告訴這個視圖,你需要重新渲染了液兽。
所以當(dāng)一個數(shù)據(jù)改變骂删,react的組件渲染是很消耗性能的——父組件的狀態(tài)更新了,所有的子組件得跟著一起渲染四啰,它不能像vue一樣,精確到當(dāng)前組件的粒度粗恢。
為了佐證柑晒,我分別用react和vue寫了一個demo,功能很簡單:父組件嵌套子組件眷射,點擊父組件的按鈕會修改父組件的狀態(tài)匙赞,點擊子組件的按鈕會修改子組件的狀態(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 (
<div>
<h2>{this.state.fatherState}</h2>
<button onClick={this.changeState}>change Father state</button>
<hr/>
<Child/>
</div>
)
}
}
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 (
<div>
<h3>{this.state.childState}</h3>
<button onClick={this.changeState}>change Child state</button>
</div>
)
}
}
ReactDOM.render(<Father/>,document.getElementById('root'))
上面是使用react時的效果欧宜,修改父組件的狀態(tài)坐榆,父子組件都會重新渲染:點擊change Father state
,不僅打印了Father:render
冗茸,還打印了child:render
席镀。
(戳這里試試在線demo)
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時的效果,無論是修改哪個狀態(tài)夏漱,組件都只重新渲染最小顆粒:點擊change Father state
豪诲,只打印Father:render
,不會打印child:render
挂绰。
(戳這里試試在線demo)
不同響應(yīng)式原理的影響
首先需要強調(diào)的是屎篱,上文提到的“渲染”“render”“更新“都不是指瀏覽器真正渲染出視圖。而是框架在javascript層面上,調(diào)用自身實現(xiàn)的render方法交播,生成一個普通的對象专肪,這個對象保存了真實dom的屬性,也就是常說的虛擬dom堪侯。本文會用組件渲染和頁面渲染對兩者做區(qū)分嚎尤。
每次的視圖更新流程是這樣的:
- 組件渲染生成一棵新的虛擬dom樹;
- 新舊虛擬dom樹對比伍宦,找出變動的部分芽死;(也就是常說的diff算法)
- 為真正改變的部分創(chuàng)建真實dom,把他們掛載到文檔次洼,實現(xiàn)頁面重渲染关贵;
由于react和vue的響應(yīng)式實現(xiàn)原理不同,數(shù)據(jù)更新時卖毁,第一步中react組件會渲染出一棵更大的虛擬dom樹。
fiber是什么
上面說了這么多亥啦,都是為了方便講清楚為什么需要react fiber:在數(shù)據(jù)更新時炭剪,react生成了一棵更大的虛擬dom樹疚沐,給第二步的diff帶來了很大壓力——我們想找到真正變化的部分,這需要花費更長的時間亮蛔。js占據(jù)主線程去做比較痴施,渲染線程便無法做其他工作,用戶的交互得不到響應(yīng)尔邓,所以便出現(xiàn)了react fiber晾剖。
react fiber沒法讓比較的時間縮短,但它使得diff的過程被分成一小段一小段的梯嗽,因為它有了“保存工作進度”的能力齿尽。js會比較一部分虛擬dom,然后讓渡主線程灯节,給瀏覽器去做其他工作循头,然后繼續(xù)比較绵估,依次往復(fù),等到最后比較完成卡骂,一次性更新到視圖上国裳。
fiber是一種新的數(shù)據(jù)結(jié)構(gòu)
上文提到了,react fiber使得diff階段有了被保存工作進度的能力全跨,這部分會講清楚為什么缝左。
我們要找到前后狀態(tài)變化的部分,必須把所有節(jié)點遍歷浓若。
在老的架構(gòu)中渺杉,節(jié)點以樹的形式被組織起來:每個節(jié)點上有多個指針指向子節(jié)點。要找到兩棵樹的變化部分挪钓,最容易想到的辦法就是深度優(yōu)先遍歷是越,規(guī)則如下:
- 從根節(jié)點開始,依次遍歷該節(jié)點的所有子節(jié)點碌上;
- 當(dāng)一個節(jié)點的所有子節(jié)點遍歷完成倚评,才認為該節(jié)點遍歷完成;
如果你系統(tǒng)學(xué)習(xí)過數(shù)據(jù)結(jié)構(gòu)馏予,應(yīng)該很快就能反應(yīng)過來天梧,這不過是深度優(yōu)先遍歷的后續(xù)遍歷。根據(jù)這個規(guī)則吗蚌,在圖中標(biāo)出了節(jié)點完成遍歷的順序腿倚。
這種遍歷有一個特點,必須一次性完成蚯妇。假設(shè)遍歷發(fā)生了中斷,雖然可以保留當(dāng)下進行中節(jié)點的索引暂筝,下次繼續(xù)時箩言,我們的確可以繼續(xù)遍歷該節(jié)點下面的所有子節(jié)點,但是沒有辦法找到其父節(jié)點——因為每個節(jié)點只有其子節(jié)點的指向焕襟。斷點沒有辦法恢復(fù)陨收,只能從頭再來一遍。
以該樹為例:
在遍歷到節(jié)點2時發(fā)生了中斷鸵赖,我們保存對節(jié)點2的索引务漩,下次恢復(fù)時可以把它下面的3、4節(jié)點遍歷到它褪,但是卻無法找回5饵骨、6、7茫打、8節(jié)點居触。
在新的架構(gòu)中,每個節(jié)點有三個指針:分別指向第一個子節(jié)點、下一個兄弟節(jié)點蟀拷、父節(jié)點志衍。這種數(shù)據(jù)結(jié)構(gòu)就是fiber,它的遍歷規(guī)則如下:
- 從根節(jié)點開始弊予,依次遍歷該節(jié)點的子節(jié)點祥楣、兄弟節(jié)點,如果兩者都遍歷了汉柒,則回到它的父節(jié)點误褪;
- 當(dāng)一個節(jié)點的所有子節(jié)點遍歷完成,才認為該節(jié)點遍歷完成竭翠;
根據(jù)這個規(guī)則振坚,同樣在圖中標(biāo)出了節(jié)點遍歷完成的順序。跟樹結(jié)構(gòu)對比會發(fā)現(xiàn)斋扰,雖然數(shù)據(jù)結(jié)構(gòu)不同渡八,但是節(jié)點的遍歷開始和完成順序一模一樣。不同的是传货,當(dāng)遍歷發(fā)生中斷時屎鳍,只要保留下當(dāng)前節(jié)點的索引,斷點是可以恢復(fù)的——因為每個節(jié)點都保持著對其父節(jié)點的索引问裕。
同樣在遍歷到節(jié)點2時中斷逮壁,fiber結(jié)構(gòu)使得剩下的所有節(jié)點依舊能全部被走到。
這就是react fiber的渲染可以被中斷的原因粮宛。樹和fiber雖然看起來很像窥淆,但本質(zhì)上來說,一個是樹巍杈,一個是鏈表忧饭。
fiber是纖程
這種數(shù)據(jù)結(jié)構(gòu)之所以被叫做fiber,因為fiber的翻譯是纖程筷畦,它被認為是協(xié)程的一種實現(xiàn)形式词裤。協(xié)程是比線程更小的調(diào)度單位:它的開啟、暫捅畋觯可以被程序員所控制吼砂。具體來說,react fiber是通過requestIdleCallback
這個api去控制的組件渲染的“進度條”鼎文。
requesetIdleCallback
是一個屬于宏任務(wù)的回調(diào)渔肩,就像setTimeout一樣。不同的是漂问,setTimeout的執(zhí)行時機由我們傳入的回調(diào)時間去控制赖瞒,requesetIdleCallback是受屏幕的刷新率去控制女揭。本文不對這部分做深入探討,只需要知道它每隔16ms會被調(diào)用一次栏饮,它的回調(diào)函數(shù)可以獲取本次可以執(zhí)行的時間吧兔,每一個16ms除了requesetIdleCallback
的回調(diào)之外,還有其他工作袍嬉,所以能使用的時間是不確定的境蔼,但只要時間到了,就會停下節(jié)點的遍歷伺通。
使用方法如下:
const workLoop = (deadLine) => {
let shouldYield = false;// 是否該讓出線程
while(!shouldYield){
console.log('working')
// 遍歷節(jié)點等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
requestIdleCallback的回調(diào)函數(shù)可以通過傳入的參數(shù)deadLine.timeRemaining()
檢查當(dāng)下還有多少時間供自己使用箍土。上面的demo也是react fiber工作的偽代碼。
但由于兼容性不好罐监,加上該回調(diào)函數(shù)被調(diào)用的頻率太低吴藻,react實際使用的是一個polyfill(自己實現(xiàn)的api),而不是requestIdleCallback弓柱。
現(xiàn)在沟堡,可以總結(jié)一下了:React Fiber是React 16提出的一種更新機制,使用鏈表取代了樹矢空,將虛擬dom連接航罗,使得組件更新的流程可以被中斷恢復(fù);它把組件渲染的工作分片屁药,到時會主動讓出渲染主線程粥血。
react fiber帶來的變化
首先放一張在社區(qū)廣為流傳的對比圖,分別是用react 15和16實現(xiàn)的酿箭。這是一個寬度變化的三角形复亏,每個小圓形中間的數(shù)字會隨時間改變,除此之外缭嫡,將鼠標(biāo)懸停蜓耻,小圓點的顏色會發(fā)生變化。
[圖片上傳失敗...(image-f88974-1647870632131)]
(戳這里是react15-stack在線地址|這里是react16-fiber)
實操一下械巡,可以發(fā)現(xiàn)兩個特點:
- 使用新架構(gòu)后,動畫變得流暢饶氏,寬度的變化不會卡頓讥耗;
- 使用新架構(gòu)后,用戶響應(yīng)變快疹启,鼠標(biāo)懸停時顏色變化更快古程;
看到到這里先稍微停一下,這兩點都是fiber帶給我們的嗎——用戶響應(yīng)變快是可以理解的喊崖,但使用react fiber能帶來渲染的加速嗎挣磨?
動畫變流暢的根本原因雇逞,一定是一秒內(nèi)可以獲得更多動畫幀。但是當(dāng)我們使用react fiber時茁裙,并沒有減少更新所需要的總時間塘砸。
為了方便理解,我把刷新時的狀態(tài)做了一張圖:
上面是使用舊的react時晤锥,獲得每一幀的時間點掉蔬,下面是使用fiber架構(gòu)時,獲得每一幀的時間點矾瘾,因為組件渲染被分片女轿,完成一幀更新的時間點反而被推后了,我們把一些時間片去處理用戶響應(yīng)了壕翩。
這里要注意蛉迹,不會出現(xiàn)“一次組件渲染沒有完成,頁面部分渲染更新”的情況放妈,react會保證每次更新都是完整的北救。
但頁面的動畫確實變得流暢了,這是為什么呢大猛?
我把該項目的 代碼倉庫 down下來扭倾,看了一下它的動畫實現(xiàn):組件動畫效果并不是直接修改width
獲得的,而是使用的transform:scale
屬性搭配3D變換挽绩。如果你聽說過硬件加速膛壹,大概知道為什么了:這樣設(shè)置頁面的重新渲染不依賴上圖中的渲染主線程,而是在GPU中直接完成唉堪。也就是說模聋,這個渲染主線程線程只用保證有一些時間片去響應(yīng)用戶交互就可以了。
-<SierpinskiTriangle x={0} y={0} s={1000}>
+<SierpinskiTriangle x={0} y={0} s={1000*t}>
{this.state.seconds}
</SierpinskiTriangle>
修改一下項目代碼中152行唠亚,把圖形的變化改為寬度width
修改链方,會發(fā)現(xiàn)即使用react fiber,動畫也會變得相當(dāng)卡頓灶搜,所以這里的流暢主要是CSS動畫的功勞祟蚀。(內(nèi)存不大的電腦謹慎嘗試,瀏覽器會卡死)
react不如vue割卖?
我們現(xiàn)在已經(jīng)知道了react fiber是在彌補更新時“無腦”刷新前酿,不夠精確帶來的缺陷。這是不是能說明react性能更差呢鹏溯?
并不是罢维。孰優(yōu)孰劣是一個很有爭議的話題,在此不做評價丙挽。因為vue實現(xiàn)精準(zhǔn)更新也是有代價的肺孵,一方面是需要給每一個組件配置一個“監(jiān)視器”匀借,管理著視圖的依賴收集和數(shù)據(jù)更新時的發(fā)布通知,這對性能同樣是有消耗的平窘;另一方面vue能實現(xiàn)依賴收集得益于它的模版語法吓肋,實現(xiàn)靜態(tài)編譯,這是使用更靈活的JSX語法的react做不到的初婆。
在react fiber出現(xiàn)之前蓬坡,react也提供了PureComponent、shouldComponentUpdate磅叛、useMemo,useCallback等方法給我們屑咳,來聲明哪些是不需要連帶更新子組件。
結(jié)語
回到開頭的幾個問題弊琴,答案不難在文中找到:
- react因為先天的不足——無法精確更新兆龙,所以需要react fiber把組件渲染工作切片;而vue基于數(shù)據(jù)劫持敲董,更新粒度很小紫皇,沒有這個壓力;
- react fiber這種數(shù)據(jù)結(jié)構(gòu)使得節(jié)點可以回溯到其父節(jié)點腋寨,只要保留下中斷的節(jié)點索引聪铺,就可以恢復(fù)之前的工作進度;
如果這篇文章對你有幫助萄窜,給我點個贊唄~這對我很重要
[圖片上傳失敗...(image-641a7c-1647870632131)]
(點個關(guān)注更好铃剔!>3<)