React16 更新了底層架構,新架構主要解決更新節點過多時,頁碼卡頓的問題。譬如以下代碼,根據用戶輸入的文字生成10000行數據,用戶輸入框會出現卡頓現象。javascript
class App extends React.Component { constructor( props ) { super( props ); this.state = { rowData: [] } } handleUserInput = (e)=>{ let userInput = e.target.value; let newRowData = []; for( let i = 0; i < 10000; i++) { newRowData.push( userInput ); } this.setState( { rowData: newRowData } ) } renderRows() { return this.rowData.map( (s,index)=>{ return ( <tr key={index}> <td>{s}</td> </tr> ) } ) } render() { return ( <div> <div> <input type="text" onChange={ this.handleUserInput }/> </div> <table> <tbody> { this.renderRows() } </tbody> </table> </div> ); } }
爲了引出瀏覽器卡頓真正的緣由,咱們先簡單介紹一個概念:FPS(Frames Per Second) - 每秒傳輸幀數。舉個例子,通常來講動畫片是如何動起來的呢?是以極快的速度連續播放靜態的圖片,利用視網膜圖像殘留效應,讓人產生動起來的錯覺。那麼這個播放要多塊呢?每秒最少要展現24張圖片,觀衆才勉強不會感覺到畫面延時(即 FPS 達到24,不會讓人以爲卡頓)。java
瀏覽器其實也是相似的原理,每間隔必定的時間從新繪製一下當前頁面。通常來講這個頻率是每秒60次。也就是說每16毫秒( 1 / 60 ≈ 0.0167 )瀏覽器會有一個週期性地重繪行爲,這每16毫秒咱們稱爲一幀。這一幀的時間裏面瀏覽器作些什麼事情呢:node
inter-frame idle period.jpg算法
這個過程是順序的,若是 JS 執行的時間過長,那麼後續的步驟也就會被相應的延後,致使的後果就是一幀的時間變長,FPS 變低。人直觀的感覺就是頁面變卡頓。回到上面的例子,一會兒更新10000條數據致使 React 執行了至關長的時間,讓瀏覽器這段時間內沒法作其餘事情,下一幀被延遲了。segmentfault
有人會想到說,誒,一次執行時間太長會卡我能理解,可是爲啥我之前用定時器作 JS 動畫有時也會卡呢?下面咱們就分析下緣由。瀏覽器
咱們把 setTimeout 和瀏覽器幀流兩條時間線放在一塊兒看一下( 綠色是 paint,紫色是 render,黃色是執行 JS ):架構
想象一下,當你不知道瀏覽器頁面繪製原理的時候是否是全憑感受來設置 setTimeout 的間隔?固然你也能夠把 setTimeout 的間隔設置成16毫秒。不過若是對 event loop 機制瞭解的話,你會知道這個只能大體保證按這個時間間隔執行,並不會嚴格保證。setInterval 也是相似,可是比 setTimeout 更不可控。dom
回過頭來咱們仔細分解下每一幀瀏覽器要作些什麼(見下圖),先是響應各類事件,而後執行 event loop 中的任務,而後是一段 raf 時間,最後是計算排版(layout)和從新繪製(paint)。大體你能夠認爲是先執行程序,而後再根據 JS 執行的結果重繪頁面,固然若是 dom 元素沒有任何變化,那麼重繪這個步驟就省了。
life of a frame.png函數
若是咱們能保證 JS 動畫的每次執行都在重繪前,那麼咱們就能作到動畫的順滑,setTimeout 沒法保證,可是瀏覽器提供了新的 API 來幫助咱們了。oop
requestAnimationFrame
這個函數的做用就是告訴瀏覽器你但願執行一段 JS,而且要求瀏覽器在下次重繪以前調用這段 JS 所在的回調函數。
requestAnimationFrame( function(){ document.body.style.width = '100px'; } )
上述代碼執行後,在瀏覽器繪製頁面的下一幀重繪前,會執行回調函數,那麼就能保證修改的 dom 的效果能在下一幀被顯示出來。回看上面的幀的生命週期,raf 時間就是留給 requestAnimationFrame 所註冊的回調函數執行用的。這樣咱們把之前的 setTimeout 動畫就能夠用 requestAnimationFrame 來改造。
// 舊版:讓元素右移500像素 function moveToRight( div ) { let left = parseInt( div.style.left ); if ( left < 500 ) { div.style.left = (left+10+'px'); setTimeout( function(){ moveToRight( div ); }, 16 ) } else { return; } } moveToRight( div ); // 新版:讓元素右移500像素 function moveToRight( div ) { let left = parseInt( div.style.left ); if ( left < 500 ) { div.style.left = (left+10+'px'); requestAnimationFrame( function(){ moveToRight( div ); } ) } else { return; } } requestAnimationFrame( function(){ moveToRight( div ); } )
特別注意:不是用了 requestAnimationFrame 後動畫就流暢了。若是你傳入 requestAnimationFrame 的回調函數執行的 JS 耗時過長,同樣會致使後續步驟的延時,引發瀏覽器 FPS 的降低。因此這點在寫代碼的時候要注意。
如今有一個問題,傳入 requestAnimationFrame 的回調函數必定是會被被安排在下一次重繪前所調用的,可是若是 raf 時間以前就已經執行了長時間的 JS,那麼我再執行這個回調豈不是雪上加霜?我能不能要求這種狀況說,個人代碼也不是很緊急,判斷下若是當前幀不「忙」,我就執行,若是幀「忙」,我能夠等下一幀之類的呢?好!下一個 API 來了。
requestIdleCallback
這個函數告訴瀏覽器,在空閒時期依次執行註冊的回調函數。什麼意思呢?上面咱們說過瀏覽器在一幀的時間裏面要作這個事,那個事,可是並非每時每刻這些事情都耗時的。譬如你打開頁面後什麼都不作,那麼一幀16毫秒以內又沒有啥 JS 須要執行又沒有大量的重繪工做,產生了有不少空餘時間。看下圖,黃色部分就是一幀內的空餘時間,當瀏覽器發現一幀有空餘時間就會看下有沒有調用 requestIdleCallback 註冊的回調函數,有的話就執行下。若是執行某個回調前看到幀結束了,那麼就等下一次有空閒時間接着執行剩餘的回調函數。
inter-frame idle period.jpg
有了 requestAnimationFrame 和 requestIdleCallback 咱們就能比之前更細粒度的控制 JS 執行的時間了。接下來咱們看下基於這個原理 React 如何優化它的更新 dom 的機制。
React 代碼中若是某處 setState 被調用引發了一系列更新,React 大體要作的是生成新的虛擬 dom 樹,而後和老的虛擬 dom 樹作比較,生成更新列表,最後根據這個列表更新真實的 dom。固然更新 dom 耗時在 JS 層面現階段是無法優化了,而生成虛擬 dom,作新老虛擬 dom 比較過程的耗時,是可能隨着應用的複雜程度而增長的。React16 以前絕大多數狀況是一次完成虛擬 dom 到真實 dom 更新的整個過程的。那麼這個過程若是在一幀裏面耗時過長,頁面就卡頓了。React16 的思路就是想利用 requestAnimationFrame 和 requestIdleCallback 兩個新 API,把一次耗時較長的更新任務分解到多個幀去執行。這樣給瀏覽器留出時間去響應頁面上的其餘事件,解決卡頓的問題。接下來看下僞代碼:
原來這段寫的匆忙且很差,從新更新了一篇講調度算法的大概實現React16性能改善的原理(二)。
原更新步驟大體爲
// 原更新步驟大體爲: setState( partialState ) { var inst = this._instance; var nextState = Object.assign( {}, inst.state, partialState ); // 根據新的 state 生成新的虛擬 dom inst.state = nextState; var nextRenderedElement = inst.render(); // 獲取上一次的虛擬 dom var prevComponentInstance = this._renderedComponent; // render 中的根節點的渲染對象 var prevRenderedElement = prevComponentInstance._currentElement; if( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) { // 更新 dom node prevComponentInstance.receiveComponent( nextRenderedElement ) } }
根據新的優化思路,React16新的更新過長大體爲:
setState( partialState ) { updateQueue.push( { instance: this, partialState: partialState } ); requestIdleCallback( doDiff ) } function doDiff( deadline ) { let nextUpdate = updateQueue.shift(); let pendingCommit = []; // 若是更新隊列裏面有更新,且時間富裕,則逐步計算出須要更新的內容 while( nextUpdate && deadline.timeRemaining()>ENOUGH_TIME ) { // 生成 fiber 節點,對比新老節點,生成更新dom的任務 pendingCommit.push( calculateDomModification(nextUpdate) ); // 把更新 dom 的任務加入待更新隊列 nextUpdate = updateQueue.shift(); } // 一次把當前時間片全部的 diff 出的更新任務都更新到 dom 上 if ( pendingCommit.lengt>0 ) { commitAllWork( pendingCommit ); } // 若是更新隊列還有更新,可是時間片耗盡了,那麼在下次空閒時間再更新 if ( nextUnitOfWork || updateQueue.length > 0 ) { requestIdleCallback( doDiff ); } }
實際代碼固然要比這個複雜的多,React 對上述調度的實現基於現實的考慮進行了優化:考慮到 1.有的更新是比較緊急的不能等空閒去完成要用 requestAnimationFrame、2.有的是能夠放到空閒時間去執行的、3.對於兩個新 API 的瀏覽器支持不是很好、4.瀏覽器默認刷新頻率的的時間片過短。React 團隊實現了一個本身的調度函數 requestAnimationFrameWithTimeout。
後續還打算更新其餘細節的內容,等研究好了再更新,譬如:1. 更新任務不是同步完成的,若是同一個節點在尚未把更新真正反應到 dom 上的時候,有來了一次 setState 怎麼辦?
2. React fiber 爲何是鏈式結構?