淺析有react fiber為什么不需要vue fiber:理解Fiber是什么及react和vue各自響應式原理


  提到react fiber,大部分人都知道這是一個react新特性,看過一些網上的文章,大概能說出“纖程”、“一種新的數據結構”、“更新時調度機制”等關鍵詞。但如果被問:

1、有 react fiber,為什么不需要 vue fiber 呢?

2、之前遞歸遍歷虛擬dom樹被打斷就得從頭開始,為什么有了react fiber就能斷點恢復呢?

  或許就不清楚了,這里我就來研究下具體如何解釋這 2 個問題。

一、什么是響應式

  響應式,直觀來說就是視圖會自動更新。如果一開始接觸前端就直接上手框架,會覺得這是理所當然的,但在“響應式框架”出世之前,實現這一功能是很麻煩的。下面我將做一個時間顯示器,用原生 js、react、vue 分別實現,代碼就不貼了,比較簡單,主要看核心業務。

1、原生 JS:使用定時器,document.getElementById 先找到 dom,再修改 dom 的 innerText

2、React:使用定時器,對內容做修改,只需要調用setState去修改數據,之后頁面便會重新渲染

3、Vue:使用定時器,我們一樣不用關注dom,在修改數據時,直接this.state=xxx修改,頁面就會展示最新的數據

二、React 和 Vue 的響應式原理

  修改數據時,react需要調用setState方法,而vue直接修改變量就行。看起來只是兩個框架的用法不同罷了,但響應式原理正在於此。

  從底層實現來看修改數據:在react中,組件的狀態是不能被修改的,setState沒有修改原來那塊內存中的變量,而是去新開辟一塊內存;而vue則是直接修改保存狀態的那塊原始內存。

  所以經常能看到react相關的文章里經常會出現一個詞"immutable",翻譯過來就是不可變的。

  數據修改了,接下來要解決視圖的更新:

1、react中,調用setState方法后,會自頂向下重新渲染組件,自頂向下的含義是,該組件以及它的子組件全部需要渲染;

2、而vue使用Object.defineProperty(vue@3遷移到了Proxy)對數據的設置setter和獲取getter做了劫持,也就是說,vue能准確知道視圖模版中哪一塊用到了這個數據,並且在這個數據修改時,告訴這個視圖,你需要重新渲染了。

  所以當一個數據改變,react的組件渲染是很消耗性能的——父組件的狀態更新了,所有的子組件得跟着一起渲染,它不能像vue一樣,精確到當前組件的粒度。

  為了佐證,分別用react和vue寫一個demo,功能很簡單:父組件嵌套子組件,點擊父組件的按鈕會修改父組件的狀態,點擊子組件的按鈕會修改子組件的狀態。

  驗證結果:

1、react 時:點擊子組件,子組件render調用;點擊父組件,父組件和子組件的render都會調用

2、vue 時:點擊子組件,子組件render;點擊父組件時,父組件render(子組件不會render,組件都只重新渲染最小顆粒)

三、不同響應式原理的影響

  首先需要強調的是,上文提到的“渲染”、“render”、“更新“都不是指瀏覽器真正渲染出視圖。而是框架在 JS 層面上,調用自身實現的render方法,生成一個普通的對象,這個對象保存了真實dom的屬性,也就是常說的虛擬dom。本文會用組件渲染和頁面渲染對兩者做區分。

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

(1)組件渲染生成一棵新的虛擬dom樹;

(2)新舊虛擬dom樹對比,找出變動的部分(也就是常說的diff算法)

(3)為真正改變的部分創建真實dom,把他們掛載到文檔,實現頁面重渲染;

  由於react和vue的響應式實現原理不同,數據更新時,第一步中react組件會渲染出一棵更大的虛擬dom樹。

四、Fiber 是什么

  上面說了這么多,都是為了方便講清楚為什么需要react fiber:在數據更新時,react生成了一棵更大的虛擬dom樹,給第二步的diff帶來了很大壓力——我們想找到真正變化的部分,這需要花費更長的時間。js占據主線程去做比較,渲染線程便無法做其他工作,用戶的交互得不到響應,所以便出現了react fiber。

  react fiber沒法讓比較的時間縮短,但它使得diff的過程被分成一小段一小段的,因為它有了“保存工作進度”的能力。js會比較一部分虛擬dom,然后讓渡主線程,給瀏覽器去做其他工作,然后繼續比較,依次往復,等到最后比較完成,一次性更新到視圖上。

1、Fiber 是一種新的數據結構

  react fiber使得diff階段有了被保存工作進度的能力,這部分會講清楚為什么。

  我們要找到前后狀態變化的部分,必須把所有節點遍歷

  在老的架構中,節點以樹的形式被組織起來:每個節點上有多個指針指向子節點。要找到兩棵樹的變化部分,最容易想到的辦法就是深度優先遍歷,規則如下:

(1)從根節點開始,依次遍歷該節點的所有子節點

(2)當一個節點的所有子節點遍歷完成,才認為該節點遍歷完成;

  如果你系統學習過數據結構,應該很快就能反應過來,這不過是深度優先遍歷的后續遍歷。根據這個規則,在圖中標出了節點完成遍歷的順序。

  這種遍歷有一個特點,必須一次性完成。假設遍歷發生了中斷,雖然可以保留當下進行中節點的索引,下次繼續時,我們的確可以繼續遍歷該節點下面的所有子節點,但是沒有辦法找到其父節點——因為每個節點只有其子節點的指向。斷點沒有辦法恢復,只能從頭再來一遍

  在遍歷到節點2時發生了中斷,我們保存對節點2的索引,下次恢復時可以把它下面的3、4節點遍歷到,但是卻無法找回5、6、7、8節點。

  在新的架構中,每個節點有三個指針:分別指向第一個子節點、下一個兄弟節點、父節點。這種數據結構就是fiber,它的遍歷規則如下:

(1)從根節點開始,依次遍歷該節點的子節點、兄弟節點,如果兩者都遍歷了,則回到它的父節點;

(2)當一個節點的所有子節點遍歷完成,才認為該節點遍歷完成;

  根據這個規則,同樣在圖中標出了節點遍歷完成的順序。跟樹結構對比會發現,雖然數據結構不同,但是節點的遍歷開始和完成順序一模一樣。不同的是,當遍歷發生中斷時,只要保留下當前節點的索引,斷點是可以恢復的——因為每個節點都保持着對其父節點的索引。

  同樣在遍歷到節點2時中斷,fiber結構使得剩下的所有節點依舊能全部被走到。

  這就是react fiber的渲染可以被中斷的原因。樹和fiber雖然看起來很像,但本質上來說,一個是樹,一個是鏈表

2、Fiber 是纖程

  這種數據結構之所以被叫做fiber,因為fiber的翻譯是纖程,它被認為是協程的一種實現形式。協程是比線程更小的調度單位:它的開啟、暫停可以被程序員所控制。具體來說,react fiber是通過requestIdleCallback這個api去控制的組件渲染的“進度條”。

  requesetIdleCallback是一個屬於宏任務的回調,就像setTimeout一樣。不同的是,setTimeout的執行時機由我們傳入的回調時間去控制,requesetIdleCallback是受屏幕的刷新率去控制。本文不對這部分做深入探討,只需要知道它每隔16ms會被調用一次,它的回調函數可以獲取本次可以執行的時間,每一個16ms除了requesetIdleCallback的回調之外,還有其他工作,所以能使用的時間是不確定的,但只要時間到了,就會停下節點的遍歷。

// 使用方法如下:
const workLoop = (deadLine) => { let shouldYield = false;// 是否該讓出線程
    while(!shouldYield){ console.log('working') // 遍歷節點等工作
        shouldYield = deadLine.timeRemaining()<1; } requestIdleCallback(workLoop) } requestIdleCallback(workLoop);

  requestIdleCallback的回調函數可以通過傳入的參數deadLine.timeRemaining()檢查當下還有多少時間供自己使用。上面的demo也是react fiber工作的偽代碼。

  但由於兼容性不好,加上該回調函數被調用的頻率太低,react實際使用的是一個polyfill(自己實現的api),而不是requestIdleCallback。

  現在,可以總結一下了:React Fiber是React 16提出的一種更新機制,使用鏈表取代了樹,將虛擬dom連接,使得組件更新的流程可以被中斷恢復;它把組件渲染的工作分片,到時會主動讓出渲染主線程。

五、React Fiber 帶來的變化

  首先放一張在社區廣為流傳的對比圖,分別是用react 15和16實現的。這是一個寬度變化的三角形,每個小圓形中間的數字會隨時間改變,除此之外,將鼠標懸停,小圓點的顏色會發生變化。

  可以發現兩個特點:

1、使用新架構后,動畫變得流暢,寬度的變化不會卡頓;

2、使用新架構后,用戶響應變快,鼠標懸停時顏色變化更快

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

  動畫變流暢的根本原因,一定是一秒內可以獲得更多動畫幀。但是當我們使用react fiber時,並沒有減少更新所需要的總時間。為了方便理解,我把刷新時的狀態做了一張圖:

  上面是使用舊的react時,獲得每一幀的時間點,下面是使用fiber架構時,獲得每一幀的時間點,因為組件渲染被分片,完成一幀更新的時間點反而被推后了,我們把一些時間片去處理用戶響應了。

  這里要注意,不會出現“一次組件渲染沒有完成,頁面部分渲染更新”的情況,react會保證每次更新都是完整的。

  但頁面的動畫確實變得流暢了,這是為什么呢?我把該項目的代碼倉庫 down下來,看了一下它的動畫實現:組件動畫效果並不是直接修改width獲得的,而是使用的transform:scale屬性搭配3D變換。如果你聽說過硬件加速,大概知道為什么了:這樣設置頁面的重新渲染不依賴上圖中的渲染主線程,而是在GPU中直接完成。也就是說,這個渲染主線程線程只用保證有一些時間片去響應用戶交互就可以了。

  如果把圖形的變化改為寬度width修改,會發現即使用react fiber,動畫也會變得相當卡頓,所以這里的流暢主要是CSS動畫的功勞。

六、React 不如 Vue?

  我們現在已經知道了react fiber是在彌補更新時“無腦”刷新,不夠精確帶來的缺陷。這是不是能說明react性能更差呢?

  並不是。孰優孰劣是一個很有爭議的話題,在此不做評價。因為vue實現精准更新也是有代價的:

1、一方面是需要給每一個組件配置一個“監視器”,管理着視圖的依賴收集和數據更新時的發布通知,這對性能同樣是有消耗的;

2、另一方面vue能實現依賴收集得益於它的模版語法,實現靜態編譯,這是使用更靈活的JSX語法的react做不到的。

  在react fiber出現之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法給我們,來聲明哪些是不需要連帶更新子組件。

  題目總結:

1、react 因為先天的不足——無法精確更新,所以需要react fiber把組件渲染工作切片;而vue基於數據劫持,更新粒度很小,沒有這個壓力

2、react fiber這種數據結構使得節點可以回溯到其父節點,只要保留下中斷的節點索引,就可以恢復之前的工作進度

學習文章:https://mp.weixin.qq.com/s/YxUV8a5qJuKuJTtMvybPAw


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM