阿里三面:靈魂拷問——有react fiber销睁,為什么不需要vue fiber呢?

提到react fiber存崖,大部分人都知道這是一個react新特性冻记,看過一些網(wǎng)上的文章,大概能說出“纖程”“一種新的數(shù)據(jù)結(jié)構(gòu)”“更新時調(diào)度機制”等關(guān)鍵詞金句。

但如果被問:

  1. 有react fiber檩赢,為什么不需要 vue fiber呢;
  2. 之前遞歸遍歷虛擬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):

  1. 原生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)式框架禾进,一切變得簡單了

  1. 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>
  1. 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'))
image

上面是使用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')
image

上面使用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ū)分嚎尤。

每次的視圖更新流程是這樣的:

  1. 組件渲染生成一棵新的虛擬dom樹;
  2. 新舊虛擬dom樹對比伍宦,找出變動的部分芽死;(也就是常說的diff算法)
  3. 為真正改變的部分創(chuàng)建真實dom,把他們掛載到文檔次洼,實現(xiàn)頁面重渲染关贵;

由于react和vue的響應(yīng)式實現(xiàn)原理不同,數(shù)據(jù)更新時卖毁,第一步中react組件會渲染出一棵更大的虛擬dom樹。

image

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é)點遍歷浓若。

image

在老的架構(gòu)中渺杉,節(jié)點以樹的形式被組織起來:每個節(jié)點上有多個指針指向子節(jié)點。要找到兩棵樹的變化部分挪钓,最容易想到的辦法就是深度優(yōu)先遍歷是越,規(guī)則如下:

  1. 從根節(jié)點開始,依次遍歷該節(jié)點的所有子節(jié)點碌上;
  2. 當(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ù)陨收,只能從頭再來一遍。

以該樹為例:

image

在遍歷到節(jié)點2時發(fā)生了中斷鸵赖,我們保存對節(jié)點2的索引务漩,下次恢復(fù)時可以把它下面的3、4節(jié)點遍歷到它褪,但是卻無法找回5饵骨、6、7茫打、8節(jié)點居触。

image

在新的架構(gòu)中,每個節(jié)點有三個指針:分別指向第一個子節(jié)點、下一個兄弟節(jié)點蟀拷、父節(jié)點志衍。這種數(shù)據(jù)結(jié)構(gòu)就是fiber,它的遍歷規(guī)則如下:

  1. 從根節(jié)點開始弊予,依次遍歷該節(jié)點的子節(jié)點祥楣、兄弟節(jié)點,如果兩者都遍歷了汉柒,則回到它的父節(jié)點误褪;
  2. 當(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é)點的索引问裕。

image

同樣在遍歷到節(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)兩個特點:

  1. 使用新架構(gòu)后,動畫變得流暢饶氏,寬度的變化不會卡頓讥耗;
  2. 使用新架構(gòu)后,用戶響應(yīng)變快疹启,鼠標(biāo)懸停時顏色變化更快古程;

看到到這里先稍微停一下,這兩點都是fiber帶給我們的嗎——用戶響應(yīng)變快是可以理解的喊崖,但使用react fiber能帶來渲染的加速嗎挣磨?

動畫變流暢的根本原因雇逞,一定是一秒內(nèi)可以獲得更多動畫幀。但是當(dāng)我們使用react fiber時茁裙,并沒有減少更新所需要的總時間塘砸。

為了方便理解,我把刷新時的狀態(tài)做了一張圖:

image

上面是使用舊的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é)語

回到開頭的幾個問題弊琴,答案不難在文中找到:

  1. react因為先天的不足——無法精確更新兆龙,所以需要react fiber把組件渲染工作切片;而vue基于數(shù)據(jù)劫持敲董,更新粒度很小紫皇,沒有這個壓力;
  2. react fiber這種數(shù)據(jù)結(jié)構(gòu)使得節(jié)點可以回溯到其父節(jié)點腋寨,只要保留下中斷的節(jié)點索引聪铺,就可以恢復(fù)之前的工作進度;

如果這篇文章對你有幫助萄窜,給我點個贊唄~這對我很重要

[圖片上傳失敗...(image-641a7c-1647870632131)]
(點個關(guān)注更好铃剔!>3<)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市查刻,隨后出現(xiàn)的幾起案子键兜,更是在濱河造成了極大的恐慌,老刑警劉巖穗泵,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件普气,死亡現(xiàn)場離奇詭異,居然都是意外死亡佃延,警方通過查閱死者的電腦和手機现诀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來履肃,“玉大人赶盔,你說我怎么就攤上這事∮芘ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵撕攒,是天一觀的道長陡鹃。 經(jīng)常有香客問我烘浦,道長,這世上最難降的妖魔是什么萍鲸? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任闷叉,我火速辦了婚禮,結(jié)果婚禮上脊阴,老公的妹妹穿的比我還像新娘握侧。我一直安慰自己,他們只是感情好嘿期,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布品擎。 她就那樣靜靜地躺著,像睡著了一般备徐。 火紅的嫁衣襯著肌膚如雪萄传。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天蜜猾,我揣著相機與錄音秀菱,去河邊找鬼。 笑死蹭睡,一個胖子當(dāng)著我的面吹牛衍菱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播肩豁,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼脊串,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蓖救?” 一聲冷哼從身側(cè)響起洪规,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎循捺,沒想到半個月后斩例,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡从橘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年念赶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恰力。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡叉谜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出踩萎,到底是詐尸還是另有隱情停局,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站董栽,受9級特大地震影響码倦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锭碳,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一袁稽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧擒抛,春花似錦推汽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至槽畔,卻和暖如春栈妆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厢钧。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工鳞尔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人早直。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓寥假,卻偏偏與公主長得像,于是被迫代替她去往敵國和親霞扬。 傳聞我的和親對象是個殘疾皇子糕韧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

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