react時間分片實現(xiàn)流程詳解
我們常說的調(diào)度,可以分為兩大模塊,時間分片和優(yōu)先級調(diào)度
- 時間分片的異步渲染是優(yōu)先級調(diào)度實現(xiàn)的前提
- 優(yōu)先級調(diào)度在異步渲染的基礎(chǔ)上引入優(yōu)先級機制控制任務(wù)的打斷、替換。
本節(jié)將從時間分片的實現(xiàn)剖析react
的異步渲染原理,閱讀本文你講可以了解
- 時間分片是什么
- 為什么需要時間分片
- 時間分片在react中是如何運行的
- 時間分片的極簡實現(xiàn)
什么是時間分片
上文提到過,時間分片其實就是一個固定而連續(xù)且有間隔的時間區(qū)間
固定:時間分片是工作時長是固定的
連續(xù):分片之間是連續(xù)的,當(dāng)前分片內(nèi)有工作沒做完,會留到下個分片繼續(xù)
有間隔:在進入下一個分片前,會有一定時間的間隔
這些解釋比較抽象,可以更加通俗去理解
固定:每天固定工作8小時
連續(xù):每天都要上班
有間隔:明天上班前會休息一段時間
為什么需要時間分片
我們知道,react
最重要,也是最耗時的任務(wù)是節(jié)點遍歷。
設(shè)想一個頁面上有一萬個DOM節(jié)點,如果我們用同步的方式一個個遍歷完需要花費多少時間。而且如果是同步遍歷的話,遍歷的過程中,JS線程一直會霸占主線程,導(dǎo)致阻塞了瀏覽器的其他線程,導(dǎo)致卡頓的情況出現(xiàn)。
換個思路解決這個遍歷問題,能不能遍歷一會,休息一會,休息的過程中就可以把主線程交還給渲染線程和事件線程,這樣就能及時渲染節(jié)點和響應(yīng)用戶事件,避免造成卡頓。
為了實現(xiàn)遍歷一會,休息一會,我們可以將整個過程分解為以下三個步驟
- 分片開啟
- 分片中斷、分片重啟
- 延遲執(zhí)行
這三個步驟與時間分片的三個特性一一對應(yīng)
實現(xiàn)分片開啟 - 固定
時間分片是獨立于React
的節(jié)點遍歷流程的,所以只需要把節(jié)點遍歷的入口函數(shù)以回調(diào)函數(shù)的形式傳入即可,這樣就可以讓時間分片來決定節(jié)點遍歷執(zhí)行時機。
// 節(jié)點遍歷的入口函數(shù) function Reconcile協(xié)調(diào)() { 節(jié)點遍歷() } function Schedule調(diào)度() { 創(chuàng)建分片(Reconcile協(xié)調(diào)) }
第一步,需要將時間分片要調(diào)度的函數(shù)抽象為一個任務(wù)對象
function 創(chuàng)建分片(需要被調(diào)度的函數(shù)) { const 新的任務(wù) = { callback: 需要被調(diào)度的函數(shù) } }
第二步,設(shè)定分片工作時長,為了方便后續(xù),可以直接計算過期時間。分片工作時長一般為5ms
,但Scheduler
會根據(jù)任務(wù)優(yōu)先級有所調(diào)整,這里為了更好理解,先默認5ms
。
const taskQueue = [] function 創(chuàng)建分片(需要被調(diào)度的函數(shù)) { const 新的任務(wù) = { callback: 需要被調(diào)度的函數(shù), expirationTime: performance.now() + 5000 } taskQueue.push(新的任務(wù)) 發(fā)起異步調(diào)度() }
每次分片的創(chuàng)建其實都是新一輪調(diào)度的開始,所以在末尾會發(fā)起異步調(diào)度
為什么用performance.now()而不用Date.now()
performance.now()
返回當(dāng)前頁面的停留時間,Date.now()
返回當(dāng)前系統(tǒng)時間。但不同的是performance.now()
精度更高,且比Date.now()
更可靠
performance.now()
返回的是微秒級的,Date.now()
只是毫秒級performance.now()
一個恒定的速率慢慢增加的,它不會受到系統(tǒng)時間的影響。Date.now()
受到系統(tǒng)時間影響,系統(tǒng)時間修改Date.now()
也會改變
實現(xiàn)分片中斷、重啟 - 連續(xù)
分片中斷
我們在第一章已經(jīng)將React的虛擬DOM結(jié)構(gòu)
從樹形結(jié)構(gòu)優(yōu)化成鏈表結(jié)構(gòu),所以能輕松使用while循環(huán)實現(xiàn)可中斷的遍歷
那么如果要將遍歷任務(wù)
和時間分片
相結(jié)合,且實現(xiàn)分片中斷
功能的話,只需要在while循環(huán)出加入分片時間過期的校驗即可
function 分片過期校驗() { return (perfromance.now() - 分片開啟時間) >= 5000 } let 需要被遍歷的幸運兒節(jié)點 = null function 構(gòu)建節(jié)點() { /** * ...在這里進行節(jié)點構(gòu)建工作 */ 需要被遍歷的幸運兒節(jié)點 = 需要被遍歷的幸運兒節(jié)點.next } function 節(jié)點遍歷() { while (需要被遍歷的幸運兒節(jié)點 != null && !分片過期校驗()) { 構(gòu)建節(jié)點() } } function Schedule調(diào)度() { 創(chuàng)建分片(Reconcile協(xié)調(diào)) }
分片重啟
分片重啟意思就是上一輪時間分片因為過期中斷了,需要重新發(fā)起一輪時間分片。
實現(xiàn)的思路是,在上一輪分片結(jié)束之后判斷是否還需要開啟下一輪分片,需要的話則重新發(fā)起一輪異步調(diào)度即可,相關(guān)參考視頻講解:進入學(xué)習(xí)
function 分片過期校驗() { return (perfromance.now() - 分片開啟時間) >= 5000 } function 分片事件循環(huán)() { let 棧頂任務(wù) = taskQueue.peek() while (棧頂任務(wù)) { if (分片過期校驗()) break const 棧頂任務(wù)回調(diào) = 棧頂任務(wù).callback() if (typeof 棧頂任務(wù)回調(diào) == 'function') { // 當(dāng)前任務(wù)還沒有執(zhí)行完,繼續(xù)搞 棧頂任務(wù).callback = 棧頂任務(wù)回調(diào) } else { // 當(dāng)前任務(wù)已執(zhí)行完,彈出隊列 taskQueue.pop() } 棧頂任務(wù) = taskQueue.peek() } // 還有任務(wù)哦 if (棧頂任務(wù)) return true return false } function 分片執(zhí)行() { 分片開啟時間 = performance.now() var 是否還有任務(wù)未執(zhí)行完畢 try { 是否還有任務(wù)未執(zhí)行完畢 = 分片事件循環(huán)() } finally { // 分片重啟 if (是否還有任務(wù)未執(zhí)行) 發(fā)起異步調(diào)度() } } function 發(fā)起異步調(diào)度() { // 這里實際上是異步執(zhí)行,看下面有間隔 分片執(zhí)行() }
重啟的條件就是判斷分片任務(wù)隊列中是否還有任務(wù),有的話就發(fā)起下一輪的時間分片
實現(xiàn)延遲執(zhí)行 - 有間隔
有間隔的本質(zhì)是延遲JS的執(zhí)行,讓瀏覽器有喘息的時間,去處理其他線程的任務(wù),哪如何把主線程控制權(quán)交還給瀏覽器呢??
可以使用異步特性發(fā)起下一輪時間分片,實現(xiàn)延遲執(zhí)行
function 發(fā)起異步調(diào)度() { // 將主線程短暫的交還給瀏覽器 setTimeout(() => { 分片執(zhí)行() }, 0) }
為什么選擇宏任務(wù)實現(xiàn)異步執(zhí)行
微任務(wù)無法真正達到交還主線程控制權(quán)的要求。
因為一輪事件循環(huán),是先執(zhí)行一個宏任務(wù),然后再清空微任務(wù)隊列里面的任務(wù),如果在清空微任務(wù)隊列的過程中,依然有新任務(wù)插入到微任務(wù)隊列中的話,還是把這些任務(wù)執(zhí)行完畢才會釋放主線程。所以微任務(wù)不合適。
時間分片異步執(zhí)行方案的演進
為什么不是setTimeout
?
因為setTimeout的遞歸層級過深的話,延遲就不是1ms,而是4ms,這樣會造成延遲時間過長
為什么不是requestAnimationFrame
?
requestAnimationFramed是在微任務(wù)執(zhí)行完之后,瀏覽器重排重繪之前執(zhí)行,執(zhí)行的時機是不準(zhǔn)確的。如果raf之前JS的執(zhí)行時間過長,依然會造成延遲
為什么不是requestIdleCallback
?
requestIdleCallback的執(zhí)行時機是在瀏覽器重排重繪之后,也就是瀏覽器的空閑時間執(zhí)行。其實執(zhí)行的時機依然是不準(zhǔn)確的,raf執(zhí)行的JS代碼耗時可能會過長
為什么是 MessageChannel
?
MessageChannel的執(zhí)行時機比setTimeout靠前
在React中,異步執(zhí)行優(yōu)先使用setImmediate
,其次是MessageChannel
,最后是setTimeout
,都是根據(jù)瀏覽器對這些的特性支持程度決定的。
時間分片簡單實現(xiàn)
下面會整合上面的所有代碼,模擬出最簡單的時間分片實現(xiàn)(不包含優(yōu)先級機制)
Scheduler.js
const taskQueue = [] let 分片開啟時間 = -1 // **時間分片核心** const 分片過期校驗 = () => { return (perfromance.now() - 分片開啟時間) >= 5000 } function 分片事件循環(huán)() { let 棧頂任務(wù) = taskQueue.peek() while (棧頂任務(wù)) { // 每執(zhí)行完一個任務(wù),都要校驗一下分片是否過期 if (分片過期校驗()) break const 棧頂任務(wù)回調(diào) = 棧頂任務(wù).callback() if (typeof 棧頂任務(wù)回調(diào) == 'function') { // 當(dāng)前任務(wù)還沒有執(zhí)行完,繼續(xù)搞 棧頂任務(wù).callback = 棧頂任務(wù)回調(diào) } else { // 當(dāng)前任務(wù)已執(zhí)行完,彈出隊列 taskQueue.pop() } 棧頂任務(wù) = taskQueue.peek() } // 還有任務(wù)哦 if (棧頂任務(wù)) return true return false } function 分片執(zhí)行() { 分片開啟時間 = performance.now() var 是否還有任務(wù)未執(zhí)行完畢 try { 是否還有任務(wù)未執(zhí)行完畢 = 分片事件循環(huán)() } finally { // **時間分片核心:分片重啟** if (是否還有任務(wù)未執(zhí)行) 發(fā)起異步調(diào)度() } } // 實例化 MessageChannel const channel = new MessageChannel() const port2 = channel.port2 channel.port1.onmessage = 分片執(zhí)行 function 發(fā)起異步調(diào)度() { // 向通道1發(fā)消息,通道1收到消息就會執(zhí)行分片任務(wù) // **時間分片核心:延遲執(zhí)行** port2.postMessage(null) } function 創(chuàng)建分片(需要被調(diào)度的函數(shù)) { // **時間分片核心:分片開啟** const 新的任務(wù) = { callback: 需要被調(diào)度的函數(shù), expirationTime: performance.now() + 5000 } taskQueue.push(新的任務(wù)) 發(fā)起異步調(diào)度() } export default { 創(chuàng)建分片, 分片過期校驗 }
ReactDOM.js
import * as Scheduler from './Scheduler' const { 創(chuàng)建分片, 分片過期校驗 } = Scheduler let 需要被遍歷的幸運兒節(jié)點 = null function 構(gòu)建節(jié)點() { /** * ...在這里進行節(jié)點構(gòu)建工作 */ 需要被遍歷的幸運兒節(jié)點 = 需要被遍歷的幸運兒節(jié)點.next } function 節(jié)點遍歷() { // **時間分片核心:分片中斷** while (需要被遍歷的幸運兒節(jié)點 != null && !分片過期校驗()) { 構(gòu)建節(jié)點() } } function Schedule調(diào)度() { 創(chuàng)建分片(Reconcile協(xié)調(diào)) } function 調(diào)度入口() { 需要被遍歷的幸運兒節(jié)點 = react應(yīng)用根節(jié)點 Schedule調(diào)度() } 調(diào)度入口()
這段時間分片的偽代碼相對于react中源碼的實現(xiàn),少了很多邏輯判斷,并且集中了起來,應(yīng)該會相對好理解很多。
如果還是覺得有點晦澀,可以重點關(guān)注偽代碼中標(biāo)有時間分片核心注釋的代碼,結(jié)合上文提到的概念理解
總結(jié)
讀完這篇文章估計你可能對時間分片的概念已經(jīng)有所有了解了,是不是覺得react16
的新特性之一時間分片,也并沒有想象中的神秘。
總的下來,時間分片就是由簡單的三個模塊組成:
- 分片開啟
- 分片中斷、重啟
- 延遲執(zhí)行
時間分片是Scheduler調(diào)度器兩大特性中的一個,另一個是任務(wù)的優(yōu)先級調(diào)度,接下來可能會花兩到三篇的篇幅去講解。在源碼閱讀的過程中,我覺得時間分片的實現(xiàn)已經(jīng)非常驚艷了,沒想到后面優(yōu)先級調(diào)度的設(shè)計對我更是無可匹敵的沖擊。
到此這篇關(guān)于react時間分片實現(xiàn)流程詳解的文章就介紹到這了,更多相關(guān)react時間分片內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React中使用Echarts無法顯示title、tooltip等組件的解決方案
這篇文章主要介紹了React中使用Echarts無法顯示title、tooltip等組件的解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03react中實現(xiàn)修改input的defaultValue
這篇文章主要介紹了react中實現(xiàn)修改input的defaultValue方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05作為老司機使用 React 總結(jié)的 11 個經(jīng)驗教訓(xùn)
這篇文章主要介紹了作為老司機使用 React 總結(jié)的 11 個經(jīng)驗教訓(xùn),需要的朋友可以參考下2017-04-04