3C科技 娛樂遊戲 美食旅遊 時尚美妝 親子育兒 生活休閒 金融理財 健康運動 寰宇綜合

Zi 字媒體

2017-07-25T20:27:27+00:00
加入好友
前言瀏覽器中的渲染引擎是單線程的,幾乎所有的操作都是在這個單線程中執行——解析渲染 DOM Tree 和 CSS Tree,解析執行 JavaScript ——除了網路操作。這個線程就是瀏覽器的主線程。單線程意味著,一段時間只做一件事,所以瀏覽器在同一時間內,其主線程只能關注於一個任務。在 Web 開發中,很多人覺得,不就是寫 HTML 和 CSS 將數據顯示出來么,So Easy !抱著這樣想法開發出來的網站,如果比較簡單的話,還不能看出差別,但是一旦頁面複雜,用戶交互變多之後,弊端就會爆發:卡頓,沒反應,容易崩潰……稍有經驗的前端工程師會知道,頁面的 DOM 改變,就會導致頁面重新計算 DOM,進行重繪或者重排,DOM 結構複雜或者頻繁操作 DOM 通常是性能瓶頸產生的原因。而網站從最開始比較簡單,開始變的越來越複雜,用戶交互也會越來越多,怎麼去減輕 DOM 操作帶來的性能損耗就變得重要起來。React 來了React 是近幾年非常火的一個前端框架,它第一次提出了 Virtual DOM 的概念。Virtual DOM 是一個 JavaScript 對象。每次,我們只需要告訴 React 下一個狀態是什麼,React 會自己構建一個新的 Virtual DOM,然後根據新舊 Virtual DOM 快速計算其差異,找出需要重繪或重排的元素,告訴瀏覽器。瀏覽器根據相關的更新,重新計算 DOM Tree,重繪頁面。我們下面看一個例子。這個例子會在頁面中創建一個輸入框,一個按鈕,一個 BlockList 組件。BlockList 組件會根據NUMBER_OF_BLOCK的數值渲染出對應數量的數字顯示框,數字顯示框顯示點擊按鈕的次數。我們最開始設置 據NUMBER_OF_BLOCK為 2 ,只渲染 2 個數字顯示框。首次渲染出頁面之後,我們點擊按鈕,頁面中的數字顯示框的值由 0 變為 1。如下圖所示:當點擊按鈕的時候,按鈕點擊次數從 0 變為 1,我們需要告訴 React 下面你要顯示 1 了,於是,通過setState操作,我們告訴 React: 下一個你需要顯示的數據是 1。然後,React 開始更新組件。對應的 Virtual DOM Tree 變化如下圖所示。黃色表示狀態被更新。我們點擊按鈕,觸發setState之後,React 就會創建一個新的 Virtual DOM,然後將新舊 Virtual DOM 進行 diff 操作,判斷哪些元素需要更新,將需要更新的元素放到更新列表中,最後遍歷更新列表更新所有的元素,這所有的過程都是 React 幫我們完成的。對瀏覽器而言,這個過程僅僅是編譯執行了一段 JavaScript 代碼而已,我們把從 setState 開始,到頁面渲染結束的瀏覽器主線程工作流程畫出來,如下圖所示。藍色粗線表示瀏覽器主線程。從獲得最新的數據,到將數據在頁面中呈現出來,可以分為兩個階段。第一個 調度階段。這個階段 React 用新數據生成新的 Virtual DOM ,遍歷 Virtual DOM ,然後通過 Diff 演算法,快速找出需要更新的元素,放到更新隊列中去。第二個 渲染階段。這個階段 React 根據所在的渲染環境,遍歷更新隊列,將對應元素更新。在瀏覽器中,就是跟新對應的 DOM 元素。除瀏覽器外,渲染環境還可以是 Native,硬體,VR 等。新問題之前,React 在官網中寫道:We built React to solve one problem: building large applications with data that changes over time.現在更新為:React is a declarative, efficient, and flexible JavaScript library for building user interfaces.所以我們看出,React 新的定位在於靈活高效的數據。但是在實際的使用中,尤其是遇到頁面結構複雜,數據更新頻繁的應用的時候,React 的表現不盡如人意。在上一個例子中,我們可以設置 NUMBER_OF_BLOCK 的值為 100000(實際情況下,可能沒有那麼多),將其變為一個「複雜」的網頁。點擊按鈕,觸發 setState,頁面開始更新。點擊輸入框,輸入一些字元串,比如 「hireact」。我們可以看到,頁面此時沒有任何的響應。等待 7 s,輸入框中突然出現了,之前輸入的 「hireact」,同時, BlockList 組件也更新了。在這等待 7 s 中,頁面不會給我任何的響應,我會以為網站崩潰了,或者電腦死機了。如果沒有讓我等待幾秒,只是等待了 0.5 秒,多等待幾個 0.5 秒之後我會在心裡默想:這是什麼破網站!顯而易見,這樣的用戶體驗並不好。將瀏覽器主線程在這 7 s 的 performance 如下圖所示:黃色部分是 JavaScript 執行時間,也是 React 佔用主線程時間,紫色部分是瀏覽器重新計算 DOM Tree 的時間,綠色部分是瀏覽器繪製頁面的時間。三種任務,佔用瀏覽器主線程 7 s,此時間內瀏覽器無法與用戶交互。但是 DOM 改變之後,瀏覽器重新計算 DOM Tree,重繪頁面是一個必不可少的階段(紫色綠色階段),瀏覽器一直都是這樣執行的。主要是黃色部分執行時間較長, 佔用了 6 s,即 React 較長時間佔用主線程,導致主線程無法響應用戶輸入。新技能 Get可以確定的是複雜度為常數的 diff 演算法還是很優秀的,主要問題出現在,React 的調度策略 -- Stack reconcile。這個策略像函數調用棧一樣,會深度優先遍歷所有的 Virtual DOM 節點,進行 Diff。它一定要等整棵 Virtual DOM 計算完成之後,才將任務出棧釋放主線程。所以,在瀏覽器主線程被 React 更新狀態任務佔據的時候,用戶與瀏覽器進行任何的交互都不能得到反饋,只有等到任務結束,才能突然得到瀏覽器的響應。React 這樣的調度策略對動畫的支持也不好。如果 React 更新一次狀態,佔用瀏覽器主線程的時間超過 16.6 ms[1],就會被人眼發現前後兩幀不連續,給用戶呈現齣動畫卡頓的效果。React 核心團隊很早之前就預知這樣的風險的存在,並且持續探索可解決的方式。基於瀏覽器對 requestIdleCallback 和 requestAnimationFrame 這兩個 API 的支持,以及其他團隊對者兩個 API 的實現,如 React Native 團隊。React 團隊實現新的調度策略 -- Fiber reconcile。Fiber 是一種輕量的執行線程,同線程一樣共享定址空間,線程靠系統調度,並且是搶佔式多任務處理,Fiber 則是自調用,協作式多任務處理。Fiber Reconcile 與 Stack Reconcile 主要有兩方面的不同。首先,使用協作式多任務處理任務。將原來的整個 Virtual DOM 的更新任務拆分成一個個小的任務。每次做完一個小任務之後,放棄一下自己的執行將主線程空閑出來,看看有沒有其他的任務。如果有的話,就暫停本次任務,執行其他的任務,如果沒有的話,就繼續下一個任務。整個頁面更新並重渲染過程分為兩個階段。Reconcile 階段。此階段中,依序遍歷組件,通過 diff 演算法,判斷組件是否需要更新,給需要更新的組件加上 tag。遍歷完之後,將所有帶有 tag 的組件加到一個數組中。這個階段的任務可以被打斷。Commit 階段。根據在 Reconcile 階段生成的數組,遍歷更新 DOM,這個階段需要一次性執行完。如果是在其他的渲染環境 -- Native,硬體,就會更新對應的元素。所以之前瀏覽器主線程執行更新任務的執行流程就變成了這樣。其次,對任務進行優先順序劃分。不是每來一個新任務,就要放棄現執行任務,轉而執行新任務。與我們做事情一樣,將任務劃分優先順序,只有當比現任務優先順序高的任務來了,才需要放棄現任務的執行。比如說,屏幕外元素的渲染和更新任務的優先順序應該小於響應用戶輸入任務。若現在進行屏幕外組件狀態更新,用戶又在輸入,瀏覽器就應該先執行響應用戶輸入任務。瀏覽器主線程任務執行流程如下圖所示。我們重寫一個組件,跟之前的一樣。一個輸入框,一個按鈕,一個 BlockList 組件。BlockList 組件會根據NUMBER_OF_BLOCK的數值渲染出對應數量的數字顯示框,數字顯示框顯示點擊按鈕的次數。將NUMBER_OF_BLOCK設置為 100000,模擬一個複雜的頁面。不同的是,使用 Fiber reconcile 調度策略,設置任務優先順序,讓瀏覽器先響應用戶輸入再執行組件更新。在對比代碼差異之前,我們先執行同樣的操作,對比一下瀏覽器的行為。setState點擊輸入框,輸入一些字元串,比如 「hireact」。我們可以看到,頁面能夠響應我們的輸入了。瀏覽器主線程的 performance 如下圖所示:可以看到,在黃色 JavaScript 執行過程中,也就是 React 佔用瀏覽器主線程期間,瀏覽器在也在重新計算 DOM Tree,並且進行重繪,截圖顯示,瀏覽器渲染的就是用戶新輸入的內容。簡單說,在 React 佔用瀏覽器主線程期間,瀏覽器也在與用戶交互。這個才是我們在網站上面期望獲得的體驗,瀏覽器總是對我的輸入有反饋。那我們的代碼改變了哪些呢?從下往上看:首先,從變成了ReactDOMFiber.render。我們使用了 ReactFiber 去渲染整個頁面,ReactFiber 會將整個更新任務分成若干個小的更新任務,然後設置一些任務默認的優先順序。每執行完一個小任務之後,會釋放主線程。其次,render 方法中返回的不再是一個被 div 元素包一層的組件列表,而是直接返回一個組件列表,這是 React 在新版中提供的新的寫法。除此之外,可以直接返回字元串和數字。像下面:render { return 'Hi, ReactFiber!' }render { return 123 }setState的不是最新狀態,而是一個 callback,這個 callback 返回最新狀態。同上,這個也是 React 新版中提供的新的寫法,同時也是推薦的寫法。最後,我們沒有直接調用setState,而是將其作為 callback 傳給了unstable_deferredUpdates這個 API。從名字就可以看出,deferredUpdates 是將更新推遲,unstable 表明現在還不穩定,在開發階段。從源代碼上看,做了一件事情,就是將傳給它的更新任務的優先順序設置為 lowpriority。所以我們將seState作為 callback 傳給了,就是告訴 React,這個setState任務,是一個 lowpriority 的任務。(需要注意的是,並不確定 React 團隊是否將或者deferredUpdates作為一個開放的介面,現在這個版本 [2] 可以通過這個 API 去設置優先順序。同時,從源代碼可以看到,React 團隊想要實現給任務設置優先順序的功能,目前只看到一個performWithPriority的介面,也還沒有實現。)我們點擊按鈕之後,將這個更新任務設置為 low priority。此時是沒有其他任務存在的,React 就開始進行狀態更新了。更新任務進入了 Reconcile 階段,我們點擊輸入框,此時,用戶交互任務來了,此任務優先順序高於更新任務,所以瀏覽器主線程將焦點放在了輸入框……。之後更新任務進入了 Commit 階段,不能將瀏覽器主線程放棄,到了最後瀏覽器渲染完成之後,將用戶在更新任務 Commit 階段的輸入以及最新的狀態顯示出來。對比 Stack Reconcile 和 Fiber Reconfile 的實現,我們可以看到 React 新的調度策略讓開發者對 React 應用有了更細節的控制。開發者,可以通過優先順序,控制不同類型任務的優先順序,提高用戶體驗,以及整個應用程序的靈活性。採用新的調度演算法之後,會將動畫的渲染任務優先順序提高,對動畫的支持會比較友好,具體例子可以看 Lin Clark 在 React Conf 2017 的演講。後記看起來 React Fiber 很厲害的樣子,如果要用的話,還是有一些問題是需要考慮的。比如說,task 按照優先順序之後,可能低優先順序的任務永遠不會執行,稱之為 starvation;比如說,task 有可能被打斷,需要重新執行,那麼某些依賴生命周期實現的業務邏輯可能會受到影響。……React Fiber 也是帶來了很多的好處的。比如說,增強了某些領域的支持,比如動畫、布局和手勢;比如說,在複雜頁面,對用戶的反饋會更及時,應用的用戶體驗會變好,簡單頁面看不到明顯的差異;比如說,api 基本上沒有變化,對現有項目很友好。……現在,React Fiber 已經通過了所有的測試,在網站 Is Fiber Ready Yet?, 上顯示,還有 4 個 warning 需要 fix。它會隨著 React 16 發布,到底效果怎麼樣,只有用過才知道了。參考資料:https://www.youtube.com/watch?v=ZCuYPiUIONs備註:[1]:只有動畫或者視頻達到 60 fps,人眼看起來才是流暢的,即平均 16.6 ms 就要完成整個頁面的重渲染,否則就會讓用戶覺得卡頓。這裡所指的動畫,不是在頁面播放一個視頻或者動畫,而是用 React 寫動畫。[2]:本文中 React 的版本是 React@16.0.0-alpha.3作者介紹:劉傑鳳,ThoughtWorks 軟體諮詢師,前 Python 愛好者,現 React 愛好者,在一大型航空公司參與 mWeb 項目。前端之巔關注「前端之巔」,緊跟前端發展,共享一線技術。各位澱粉投稿請發郵件到 editors@cn.infoq.com,註明「前端之巔投稿」。活動推薦:2017 年 8 月 10-11 日,聽雲聯合極客邦科技、InfoQ 將共同主辦國內第二屆應用性能管理大會 -APMCon 2017,會議的演講內容聚焦行業內最新的技術和最接地氣的實踐案例,共同探討 APM 相關的性能優化、技術方案以及創新思路,為更多的行業從業者指點應用效能提升的迷津。為回饋 InfoQ 讀者,特奉上優惠碼:APMCon_0810,立減 99 元!進入大會官網 ,了解更多專家信息吧!

本文由yidianzixun提供 原文連結

寫了 5860316篇文章,獲得 23313次喜歡
精彩推薦