欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

react?Scheduler?實現(xiàn)示例教程

 更新時間:2022年09月17日 10:21:30   作者:fangjiapeng  
這篇文章主要為大家介紹了react?Scheduler?實現(xiàn)示例教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

最近在看react源碼,react構建fiber樹這一塊邏輯還比較好理解,但是一旦涉及到任務調度相關的邏輯,看起來是一頭霧水。在參考了一些資料和react scheduler源碼后,我決定來實現(xiàn)一個簡單版的scheduler,相信跟著本文的思路實現(xiàn)一遍,就可以理解為什么react需要有scheduler這個東西來調度任務。

簡單的背景知識:

我們知道現(xiàn)在大部分設備的幀率都是60fps,也就是說瀏覽器每16.7ms會繪制一次。如果頁面上有一些動畫,那么16.7s繪制一次,看起來是比較流暢的。

簡單的css動畫

先來寫一個簡單的css動畫:一個普通的div左右滑動

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #block {
            width: 50px;
            height: 50px;
            margin: 0 0;
            background-color: #ddd;
            animation: move 5s linear infinite;
            position: absolute;
        }
        @keyframes move {
            0% {
                left: 0;
            }
            25% {
                left: 100px;
            }
            50% {
                left: 200px;
            }
            75% {
                left: 100px;
            }
            100% {
                left: 0;
            }
        }
    </style>
</head>
<body>
    <div id="block"></div>
</body>
</html>

使用谷歌瀏覽器的性能錄制面板可以看到:

在主線程上,一幀的時間是16.7ms,我們放大看看一幀時間里面,瀏覽器做了什么:

完成一次繪制需要執(zhí)行Schedule Style Recalculation, Recalculate Style, Layout, Pre-Paint, Paint, Composite Layers。這里我們不細究在每個階段瀏覽器做了什么,只需要關注這個渲染是在主線程上進行,由CPU完成的就行了。通常每16.7ms瀏覽器會繪制一次,但是如果本輪事件循環(huán)有任務在執(zhí)行,那么需要等任務執(zhí)行完再進行繪制。如果任務耗時過長,繪制次數(shù)就會變少,也就是所謂“掉幀”。因為我們現(xiàn)在頁面非常簡單,沒有js任務,所以瀏覽器每16.7ms繪制一次,動畫看起來很流暢。

現(xiàn)在我們來加上一個按鈕,點擊之后會創(chuàng)建5個任務,每個任務耗時20ms,并且馬上執(zhí)行。

<body>
    <button id="btn">click me</button>
</body>

綁定事件:

const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 5; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = new Date().getTime();
    while (new Date().getTime() - start < 20) {}
}
function flushWork(){
    while(works.length){
        const work = works.shift();
        work.call(null);
    }
}

點擊按鈕會發(fā)現(xiàn),正在滑動的div卡頓了一下,通過下圖可以看到,瀏覽器直到5個宏任務完成后才會執(zhí)行渲染,在這段時間里面,頁面不能更新,也不能響應用戶操作。

etTimeout來實現(xiàn)

如果點擊按鈕要執(zhí)行成千上百個任務,那么瀏覽器會卡死很長一段時間,這顯然是不能接受的。最簡單的改造方法是執(zhí)行一個任務后,把后續(xù)的任務處理放到下一個事件循環(huán),讓瀏覽器可以在本輪事件循環(huán)執(zhí)行繪制。精通瀏覽器原理的你肯定知道可以利用setTimeout來實現(xiàn):

const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 50; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = Date.now();
    while (Date.now() - start < 20) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        // 只執(zhí)行一個任務,后面的下個事件循環(huán)再處理
        setTimeout(workLoop, 0);
    }
}

打開控制臺分析一下:

現(xiàn)在可以看到,現(xiàn)在每個宏任務都沒有連在一起,它們在不同的事件循環(huán)里執(zhí)行。每個任務完成后,瀏覽器都會執(zhí)行一次繪制,就算要執(zhí)行的任務非常多,動畫也不會卡住不動了。

但是,仔細觀察一下,后面的宏任務間隔好像都比較大,放大看間隔大概是4ms左右。我們現(xiàn)在一個任務的執(zhí)行時間是20ms,超過了16.7ms,事實上頁面已經(jīng)有一點卡頓了。主線程資源這么緊張,每個事件循環(huán)居然還要浪費4ms,這肯定是不能接受的。很多人應該都聽說過setTimeout的最小延時限制,大概意思就是雖然你是setTimeout零秒,實際上嵌套多層之后,至少要過4ms左右,宏任務才會進入到任務隊列。

循環(huán)處理

setTimeout不能用了,有其他替代方案嗎?答案是有的,我們可以使用MessageChannel來把任務放到宏任務隊列。 MessageChannel的用法就不詳細介紹了,簡單地說,就是利用這個api,我們可以監(jiān)聽一個message事件,當事件觸發(fā)的時候,事件處理函數(shù)這個任務會加入到宏任務隊列。對應我們的例子,我們就可以綁定onmessage的時候執(zhí)行workLoop, 在workLoop里面只執(zhí)行一個任務,如果還有任務沒有執(zhí)行,那就postMessage,在下一個事件循環(huán)繼續(xù)處理。

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = workLoop;
const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 50; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const start = Date.now();
    while (Date.now() - start < 20) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        port2.postMessage(null);
    }
}

重新執(zhí)行后再分析一下,宏任務之間基本沒有間隔了:

目前我們的最小任務單元的執(zhí)行時間是20ms。因為超過了16.7ms會導致頁面變卡頓,所以實際上我們應該確保單個任務不能超過16.7ms。假設經(jīng)過合理的設計,我們的最小任務單元執(zhí)行時間不會超過2ms(這里隨機設置成1ms或2ms)。然后再來看看點擊按鈕后執(zhí)行1000個任務會怎么樣。

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = workLoop;
const works = [];
const btn = document.getElementById('btn');
btn.onclick = function () {
    for (let i = 0; i < 1000; i++) {
        works.push(macroTask)
    }
    flushWork();
}
function macroTask(){
    const time = [1, 2]; 
    const zeroOrOne = Math.round(Math.random());
    const start = Date.now();
    while (Date.now() - start < time[zeroOrOne]) {}
}
function flushWork(){
    workLoop();
}
function workLoop(){
    const work = works.shift();
    if(work){
        work.call(null);
        port2.postMessage(null);
    }
}

分析運行結果,可以看到現(xiàn)在瀏覽器繪制的幀率還是沒有60fps,我們的任務占據(jù)主線程時間太長了。所以我們需要一種機制,使得在一幀的時間內盡可能執(zhí)行多個任務,而且留有充足的時間給瀏覽器繪制頁面和響應用戶交互。

最終我們的設計方案是:在一個事件循環(huán)里面,我們只占用主線程5ms, 超過5ms就把主線程控制權交還給瀏覽器,在下一個事件循環(huán)處理任務。

具體思路

聲明一個全局隊列taskQueue存放任務;

聲明一個全局變量startTime表示任務調度的開始時間, 當接受到onmessage事件時,獲取當前時間賦值給startTime,然后開始調度任務;

調度任務:從taskQueue隊列中取出一個任務,獲取當前時間currentTime, 計算currentTime - startTime,如果大于或等于5ms,說明調度任務時長已經(jīng)達到5ms了,break出循環(huán),如果隊列里還有任務,postMessage交出主線程控制權,等下個事件循環(huán)再調度任務。

瀏覽器繪制完頁面,響應用戶交互后,在下一個事件循環(huán)再次調度任務,重新計算currentTime,startTime,此時它們的差值一定不會超過5ms, 取出一個任務執(zhí)行,然后更新currentTime。再次進入while循環(huán),判斷currentTime - startTime是否大于5ms, 大于5ms就交出控制權,否則繼續(xù)執(zhí)行下一個任務。

改造后的代碼:

const channel = new MessageChannel();
const port2 = channel.port2;
const port1 = channel.port1;
port1.onmessage = performWorkUntilDeadline;
const taskQueue = [];
let startTime = -1;
const frameYieldMs = 5; // 任務的連續(xù)執(zhí)行時間不能超過5ms
let currentTask = null; // 用來保存當前的任務
btn.onclick = function () {
    for (let i = 0; i < 1000; i++) {
        taskQueue.push(macroTask)
    }
    // 在下個事件循環(huán)開始調度任務
    port2.postMessage(null);
}
function performWorkUntilDeadline() {
    startTime = performance.now(); // 更新開始時間
    let hasMoreWork = true;
    try {
        hasMoreWork = flushWork();
    } finally {
        currentTask = null;
        if(hasMoreWork) {
            port2.postMessage(null);
        }
    }
}
function flushWork(){
    return workLoop();
}
function workLoop() {
    // 這里用currentTask全局變量來保存當前任務看起來似乎有點丑。
    // 其實是為了后續(xù)實現(xiàn)任務優(yōu)先級和任務插隊功能,先不管,就這么寫。
    currentTask = taskQueue[0];
    while(currentTask) {
        if(shouldYieldToHost()) {
            break;
        }
        currentTask.call(null);
        taskQueue.shift(); // 執(zhí)行完的任務從隊列中刪除
        currentTask = taskQueue[0]; // 繼續(xù)拿下一個任務
    }
    if(currentTask) {
        // 還有任務需要在下個事件循環(huán)處理
        return true;
    }
}
function shouldYieldToHost() {
    // 是否應該掛起任務
    const currentTime = performance.now();
    if(currentTime - startTime < frameYieldMs) {
        return false;
    }
    return true;
}
function macroTask(){
    const time = [1, 2]; 
    const zeroOrOne = Math.round(Math.random());
    const start = performance.now();
    while (performance.now() - start < time[zeroOrOne]) {}
}

好了我們再看看運行結果:瀏覽器的幀率現(xiàn)在已經(jīng)可以保持在60fps了,效果已經(jīng)很不錯了。但是目前我們的任務隊列只是一個普通的先進先出隊列,并沒有實現(xiàn)優(yōu)先級和任務插隊功能。下一篇文章我們將繼續(xù)跟著react的實現(xiàn)思路,用最小堆來實現(xiàn)優(yōu)先隊列。

以上就是react Scheduler 實現(xiàn)示例教程的詳細內容,更多關于react Scheduler 教程的資料請關注腳本之家其它相關文章!

相關文章

  • React狀態(tài)管理器Rematch的使用詳解

    React狀態(tài)管理器Rematch的使用詳解

    rematch是對redux的二次封裝,簡化了redux是使用,極大的提高了開發(fā)體驗,這篇文章主要介紹了React狀態(tài)管理器Rematch的使用,需要的朋友可以參考下
    2022-09-09
  • react中antd Upload手動上傳的示例

    react中antd Upload手動上傳的示例

    本文主要介紹了react中antd Upload手動上傳的示例,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • react中常見hook的使用方式

    react中常見hook的使用方式

    這篇文章主要介紹了react中常見hook的使用方式與區(qū)別,幫助大家更好的理解和學習使用react,感興趣的朋友可以了解下
    2021-04-04
  • React 父子組件通信的實現(xiàn)方法

    React 父子組件通信的實現(xiàn)方法

    這篇文章主要介紹了React 父子組件通信的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-12-12
  • react中history(push,go,replace)切換路由方法的區(qū)別及說明

    react中history(push,go,replace)切換路由方法的區(qū)別及說明

    這篇文章主要介紹了react中history(push,go,replace)切換路由方法的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-10-10
  • React 封裝自定義組件的操作方法

    React 封裝自定義組件的操作方法

    React中自定義組件的重要性在于它們提供了代碼重用、降低耦合性、提升可維護性、更好的團隊協(xié)作、靈活性和易于測試和調試等好處,從而提高了開發(fā)效率和質量,這篇文章主要介紹了React 封裝自定義組件,需要的朋友可以參考下
    2023-12-12
  • 比ant更豐富Modal組件功能實現(xiàn)示例詳解

    比ant更豐富Modal組件功能實現(xiàn)示例詳解

    這篇文章主要為大家介紹了比ant更豐富Modal組件功能實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • React組件的使用詳細講解

    React組件的使用詳細講解

    React組件分為函數(shù)組件與class組件;函數(shù)組件是無狀態(tài)組件,class稱為類組件;函數(shù)組件只有props,沒有自己的私有數(shù)據(jù)和生命周期函數(shù);class組件有自己私有數(shù)據(jù)(this.state)和生命周期函數(shù)
    2022-11-11
  • 詳細分析React 表單與事件

    詳細分析React 表單與事件

    這篇文章主要介紹了React 表單與事件的相關資料,文中示例代碼非常詳細,幫助大家更好的理解和學習,感興趣的朋友可以了解下
    2020-07-07
  • React路由攔截模式及withRouter示例詳解

    React路由攔截模式及withRouter示例詳解

    這篇文章主要為大家介紹了React路由攔截模式及withRouter示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08

最新評論