React利用scheduler思想實現(xiàn)任務的打斷與恢復
前言
react 16.8版本引入了fiber架構,在fiber架構的支持下,實現(xiàn)了并發(fā)渲染。
在16.8版本以前,在state改變后,經過一系列的流程,最終會進入diff,在diff的時候,會采用DFS進行遍歷虛擬dom樹,這個遍歷過程是一口氣完成的,當我們的應用很復雜,元素比較多的時候,diff的過程往往會花費很長的時間,主線程此時正在進行diif比較,如果diff事件超過16.6ms,會造成瀏覽器掉幀,如果時間很長,會造成頁面卡頓。
在react18版本,默認開啟了并發(fā)模式,開發(fā)者可以使用useTransition hook手動開并發(fā)模式,在并發(fā)模式下,diff過程是可中斷的,如果diff時間超過5ms,那么React會中斷此次diff,會交出主線程的使用權,從而讓瀏覽器去渲染,當有空閑的時間的時候,會利用時間循環(huán)機制恢復diif,從而提高大型應用的用戶體驗。并發(fā)模式下任務的打斷與恢復機制是scheduler提供的,我們今天將探討下,如何利用React scheduler打斷一個耗時任務,例如從0累加到2000萬。
scheduler
從0累加到2000萬
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount; currentIndex++) {
sum += currentIndex
}
}
火焰圖

我們看上面的火焰圖,這個任務執(zhí)行了近100ms,左右,超過了瀏覽器的一幀16.6ms,當一個任務執(zhí)行時間超過50ms時,瀏覽器會任務這個任務時長任務,超過50ms的部分,火焰圖會標記為紅色。
既然同步遍歷會造成瀏覽器掉幀,那么我們有什么辦法做到不讓瀏覽器掉幀呢?答案就是將這2000萬個累加操作分成很小的task,然后讓task作為一個工作單元去執(zhí)行,當執(zhí)行到一定的數量的時候,如果運行時間超過了我們規(guī)定的時間,那么我們中斷這個任務,在中斷的時候判斷下還有沒有任務要執(zhí)行,如果有任務要執(zhí)行,那么我們將這個任務執(zhí)行函數放入事件循環(huán)中,等下一次事件循環(huán)的時候,取出來執(zhí)行這個任務。
如何讓for循環(huán)中斷呢? 我們只需要在遍歷的時候判斷當前的索引和數據總數以及抽象出一個方法,判斷應不應該終止(比如:我規(guī)定讓task累計運行5ms,當5ms用完的時候,就應該終止)。
const getCurrentTime = () => Date.now();
// 調度應該被中斷嗎
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
// 如果當前時間減去開始時間小于 5ms, 那么繼續(xù)調度
if (timeElapsed < 5) {
return false;
}
return true;
}
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount && !shouldYieldToHost(); currentIndex++) {
sum += currentIndex
}
}
如何判斷有沒有任務需要恢復呢? 我們將work函數放在workloop函數中去執(zhí)行,當work函數終止的時候,調用?;氐?code>workloop函數時,判斷currentIndex < totalCount,如果小于,那么我們 reutrn true,告訴調用workloop的函數還有任務需要執(zhí)行,需要將任務執(zhí)行函數放到異步任務中,等下一次事件循環(huán)的時候取出來執(zhí)行。
const flushWork = () => {
return workLoop()
}
const workLoop = () => {
while(true) {
try {
work()
} catch (error) {
}finally {
if(currentIndex < totalCount) {
return true
} else {
return false
}
}
}
}
任務恢復機制,利用事件循環(huán)原理
const performWorkUntilDeadline = () => {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true; // 有剩余時間
let hasMoreWork = true;
try {
// 這里執(zhí)行的函數就是 flushWork,flushWork 如果返回一個 true 那么表示還有任務
// 這里是 workLoop 循環(huán)里 return 的, 如果 return true, 那么表示還有剩余的任務,只是時間用完了,被中斷了
hasMoreWork = flushWork();
} finally {
//如果還有剩余任務,調用schedulePerformWorkUntilDeadline將performWorkUntilDeadline 放入到異步任務里,等下一次事件循環(huán)被調用。
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
}
}
let schedulePerformWorkUntilDeadline;
// react 中調度的優(yōu)先級 setImmediate > MessageChannel > setTimeout
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}完整實現(xiàn)代碼
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null;
let startTime; // 記錄開始時間
let sum = 0
const currentIndex = 0; // 當前遍歷的索引
const totalCount = 20000000
const getCurrentTime = () => Date.now();
// 調度應該被中斷嗎
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
// 如果當前時間減去開始時間小于 5ms, 那么繼續(xù)調度
if (timeElapsed < 5) {
return false;
}
return true;
}
const performWorkUntilDeadline = () => {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true; // 有剩余時間
let hasMoreWork = true;
try {
// 這里執(zhí)行的函數就是 flushWork,flushWork 如果返回一個 true 那么表示還有任務
// 這里的 是 workLoop 循環(huán)里 return 的, 如果 return true, 那么表示還有剩余的任務,只是時間用完了,被中斷了
hasMoreWork = flushWork();
} finally {
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
}
}
};
let schedulePerformWorkUntilDeadline;
// react 中調度的優(yōu)先級 setImmediate > MessageChannel > setTimeout
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
const flushWork = () => {
return workLoop()
}
const workLoop = () => {
while(true) {
try {
work()
} catch (error) {
}finally {
if(currentIndex < totalCount) {
return true
} else {
return false
}
}
}
}
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount && !shouldYieldToHost(); currentIndex++) {
sum += currentIndex
}
}
performWorkUntilDeadline()
火焰圖


我們可以看到,上面執(zhí)行近100ms的任務被分成了很多很多小任務,其中每個小任務執(zhí)行的時間是5ms左右。這樣我們就完成了利用react scheduler實現(xiàn)任務的打斷與恢復機制。
以上就是React利用scheduler思想實現(xiàn)任務的打斷與恢復的詳細內容,更多關于React scheduler任務的打斷與恢復的資料請關注腳本之家其它相關文章!
相關文章
React Router 中實現(xiàn)嵌套路由和動態(tài)路由的示例
React Router 是一個非常強大和靈活的路由庫,它為 React 應用程序提供了豐富的導航和 URL 管理功能,能夠幫助我們構建復雜的單頁應用和多頁應用,這篇文章主要介紹了React Router 中如何實現(xiàn)嵌套路由和動態(tài)路由,需要的朋友可以參考下2023-05-05
React?Hook?Form?優(yōu)雅處理表單使用指南
這篇文章主要為大家介紹了React?Hook?Form?優(yōu)雅處理表單使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03

