由節流函數引起出對event-loop的思考,順便刷刷爆款題

引子

當我在看節流函數的時候,碰到了setTimtout,因而從js運行機制挖到了event-loop。那麼我們就先從這個簡單的節流函數看起。html

// 節流:若是短期內大量觸發同一事件,那麼在函數執行一次以後,該函數在指定的時間期限內再也不工做,直至過了這段時間才從新生效。
function throttle (fn, delay) {
    let sign = true;
    return function () {    // 閉包,保存變量的值,防止每次執行次函數,值都被重置
        if (sign) {
            sign = false;
            setTimeout (() => {
                fn();
                sign = true;
            }, delay);
        } else {
            return false;
        }
    }
}
window.onscroll = throttle(foo, 1000);
複製代碼

那麼這個節流函數是怎麼實現的節流呢?前端

讓咱們來看一下它的執行步驟(假設咱們一直不停的在滾動):chrome

  1. 當咱們打開頁面,代碼執行到window.onscroll = throttle(foo, 1000)就會直接執行 throttle函數,定義了一個變量 sign 爲 true,而後碰到了 return 跳出 throttle函數,並返回另外一個匿名函數。
  2. 而後咱們滾動頁面,那麼就會觸發 onscroll 事件,執行 throttle函數。而此時咱們的 throttle函數,實際就是執行 return 的那個匿名函數。由於閉包的緣故,保存了 sign的值(感受還要填個閉包的坑...),此時的sign 是 true。就執行 if判斷,把sign 改成 false。而後碰到了定時器,咱們如今不用管定時器的回調函數的內容。
  3. 咱們還一直在滾動,那麼又觸發了 onscroll事件,因而繼續進行 if else 判斷。此時 sign 已是false了,什麼都沒有發生。
  4. 繼續,咱們一直不停的在滾動,仍是觸發了 onscroll事件,由於 sign 仍是false,因此仍是什麼都沒有發生。
  5. 一直重複步驟4,直到1s之後的那個 onscroll事件執行完成後,咱們的setTimeout被執行了,首先執行了咱們的須要被執行的fn()函數,而後把 sign置爲 true。又開始跟前面同樣,執行 if判斷了。

那麼爲何在執行了 if判斷的過程當中,碰到了setTimeout,咱們的sign並無被改成true,從而一直的執行 if判斷呢?那麼就須要聊一聊js的運行機制了。終於要進正題了,真不容易...segmentfault

js運行機制

先看一下阮一峯大佬的promise

(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。瀏覽器

(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。bash

(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。閉包

(4)主線程不斷重複上面的第三步。異步

我本身歸類就是js中有:async

  • 同步任務和異步任務

  • 宏任務(macrotask)和微任務(microtask)

  • 主線程(同步任務) - 全部同步任務都在主線程上執行,造成一個執行棧。

  • 任務隊列(異步任務):當異步任務有告終果,就在任務隊列中放一個事件。

  • JS運行機制:當"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列"

其中宏任務包括:script(主代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering

微任務包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver

這裏咱們注意到,宏任務裏有 script,也就是咱們的正常執行的主代碼。

事件循環 event-loop

主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)。此機制具體以下:主線程會不斷從任務隊列中按順序取任務執行,每執行完一個任務都會檢查microtask隊列是否爲空(執行完一個任務的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去任務隊列中取下一個任務執行。

我又給總結了一下籠統的過程:script(宏任務) - 清空微任務隊列 - 執行一個宏任務 - 清空微任務隊列 - 執行一個宏任務, 如此往復。

  • 先執行script裏的同步代碼(此時是宏任務)。碰到異步任務,放到任務隊列。
  • 查找任務隊列有沒有微任務,有就把此時的微任務所有按順序執行 (這就是爲何promise會比setTimeout先執行,由於先執行的宏任務是同步代碼,setTimeout被放進任務隊列了,setTimeout又是宏任務,在它以前先得執行微任務(就好比promise))。
  • 執行一個宏任務(先進到隊列中的那個宏任務),再把此次宏任務裏的宏任務和微任務放到任務隊列。
  • ...一直重複二、3步驟

要作到心中有隊列,有先進先出的概念

借用前端小姐姐的一張圖來解釋:

event-loop2

如今再看開頭的節流函數,就明白爲何碰到了setTimeout,咱們的sign並無被改成true了把。

那咱們繼續,看一下最近看到的爆款題。

開始闖關

第一關

看這段代碼

console.log('script start');

setTimeout(() => {
    console.log('setTimeout1');
}, 0);

new Promise((resolve) => {
    resolve('Promise1');
}).then((data) => {
    console.log(data);
});

new Promise((resolve) => {
    resolve('Promise2');
}).then((data) => {
    console.log(data);
});

console.log('script end');
複製代碼

對照這上面的執行過程不可貴出結論,script start -> script end -> Promise1 -> Promise2 -> setTimeout1

就算 setTimeout 不延時執行,它也會在 Promise以後執行,誰讓js就是先執行同步代碼,而後去找微任務再去找宏任務了呢。

懂了這裏,那咱們繼續咯。

第二關

setTimeout(() => {
    console.log('setTimeout1');

    setTimeout(() => {
        console.log('setTimeout3');
    }, 0);

    Promise.resolve().then(data=>{
        console.log('setTimeout 裏的 Promise');
    });
}, 0);

setTimeout(() => {
    console.log('setTimeout2');
}, 0);

Promise.resolve().then(() => {
    console.log('Promise1');
});
複製代碼

根據前面的流程

  1. 執行script,看到了第一個 setTimeout 放入任務隊列,看到了第二個 setTimeout 放到任務隊列。看到了Promise.then() 放到任務隊列,並無同步代碼。
  2. 檢查微任務,發現了 Promise.then() 打印Promise1
  3. 檢查發現沒有別的微任務了,檢查宏任務,此時有兩個宏任務(兩個setTimeout),可是規則告訴咱們,只執行一個宏任務,由於隊列是先進先出的原則,執行先進入隊列的那個 setTimeout,打印 setTimeout1。又發現了 一個 setTimeout,放進任務隊列。看見了 Promise.then() ,打印setTimeout 裏的 Promise
  4. 檢查宏任務,發現了宏任務,執行先進的那個,因此打印setTimeout2
  5. 檢查微任務,沒有。
  6. 檢查宏任務,打印setTimeout3

搞清楚了這個,那咱們再繼續玩兒玩兒?

第三關

console.log('script start');

setTimeout(() => {
    console.log('setTimeout1');
}, 0);

new Promise((resolve) => {
    console.log('Promise3');
    resolve();
}).then(() => {
    console.log('Promise1');
});

new Promise((resolve) => {
    resolve();
}).then(() => {
    console.log('Promise2');
});

console.log('script end');
複製代碼

再來看看這個代碼的執行結果呢。

script start -> Promise3 -> script end -> Promise1 -> Promise2 -> setTimeout1

有些朋友可能會說,不是說好了 Promise 是微任務,要在主代碼執行之後才執行嘛,你個 Promise3 咋叛變了。

其實 Promise3 沒有叛變,以前說的 Promise微任務是.then()執行的代碼。而在new Promise的回調函數裏的代碼是同步任務。

第四關

咱們繼續看關於promise的

setTimeout(()=>{
    console.log(1) 
},0);

let a=new Promise((resolve)=>{
    console.log(2)
    resolve()
}).then(()=>{
    console.log(3) 
}).then(()=>{
    console.log(4) 
});

console.log(5);
複製代碼

這個輸出 2 -> 5 -> 3 -> 4 -> 1。你想對了嘛?

這個要從Promise的實現來講,Promise的executor是一個同步函數,即非異步,當即執行的一個函數,所以他應該是和當前的任務一塊兒執行的。而Promise的鏈式調用then,每次都會在內部生成一個新的Promise,而後執行then,在執行的過程當中不斷向微任務(microtask)推入新的函數,所以直至微任務(microtask)的隊列清空後纔會執行下一波的macrotask。

第五關

promise繼續進化

new Promise((resolve,reject)=>{
    console.log("promise1")
    resolve()
}).then(()=>{
    console.log("then11")
    new Promise((resolve,reject)=>{
        console.log("promise2")
        resolve()
    }).then(()=>{
        console.log("then21")
    }).then(()=>{
        console.log("then23")
    })
}).then(()=>{
    console.log("then12")
})
複製代碼

直接上解釋吧。

遇到這種嵌套式的Promise不要慌,首先要心中有一個隊列,可以將這些函數放到相對應的隊列之中。

Ready GO

第一輪

  • current task: promise1是當之無愧的當即執行的一個函數,參考上一章節的executor,當即執行輸出[promise1]
  • micro task queue: [promise1的第一個then]

第二輪

  • current task: then1執行中,當即輸出了then11以及新promise2的promise2
  • micro task queue: [新promise2的then函數,以及promise1的第二個then函數]

第三輪

  • current task: 新promise2的then函數輸出then21和promise1的第二個then函數輸出then12
  • micro task queue: [新promise2的第二then函數]

第四輪

  • current task: 新promise2的第二then函數輸出then23
  • micro task queue: []

END

可能有人會對第二輪的隊列表示疑問,爲何是 」新promise2的then函數「 先進了隊列,而後纔是 」promise1的第二個then函數「 進入隊列?」新promise2的第二then函數「 爲何有沒有在這一輪中進入到隊列中來呢?

看不懂不要緊,咱們來調試一下代碼:

在打印完 promise2 之後,19行先執行到了 })這裏,而後到了then這裏。

再下一步,到了 promise1的第二個})這裏了。並無執行20行的console.log。

由此看出:promise2的第一個then進入任務隊列中了。並無被執行.then()。

繼續執行,打印 then21

由此得出:promise1的第二個then放入異步隊列中,並無被執行。程序執行到這裏,宏任務算是執行完了。檢查微任務,此時隊列中放着 [ '新promise2的then函數', 'promise1的第二個then函數'] ,也就是第二輪所寫的隊列。

這一步,到了promise2的二個then前面的})

往下執行到了這裏,又碰到了異步,放入隊列中去。

此時隊列: [ 'promise1的第二個then函數' ,'promise2的第二個then函數' ]

打印 promise1 的 then12

先進先出,因此先執行了 'promise1的第二個then函數' 。

此時隊列: [ 'promise2的第二個then函數' ]

最後才輸出了 then23


第六關 async/await

截至到上一關,我本覺得我已經徹底掌握了event-loop。後來我看到了 async/await , async await是generatorPromise 的語法糖這個你們應該都知道,可是打印以後跟我預期的不太同樣,頓時有點兒蒙圈,後來一分析,原來如此。

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async  function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 
複製代碼

這段代碼也算是網紅代碼了,我已經不下三個地方見過了...

先仔細想想應該輸出什麼,而後打印一下看看。(chrome 73版本打印結果)

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout
複製代碼

直接從async開始看起吧。

當程序執行到了async1();的時候

  • 首先輸出async1 start

  • 執行到await async2();,會從右向左執行,先執行async2(),打印async2,看見await,會阻塞代碼去執行同步任務。

async/await僅僅影響的是函數內的執行,而不會影響到函數體外的執行順序。也就是說async1()並不會阻塞後續程序的執行,await async2()至關於一個Promise,console.log("async1 end");至關於前方Promise的then以後執行的函數。

如此一來,就能夠得出上面的結果了。

可是,你也許打印出來會是下面這樣的結果:

clipboard.png

這個就跟V8有關係了(在chrome 71版本中,我打印出的是圖片中的結果)。至於async/await和promise到底誰會先執行,這裏偷個懶,你們看 小美娜娜:Eventloop不可怕,可怕的是趕上Promise裏的版本5有很是詳細的解讀。

參考文章:

安歌:淺談js防抖和節流

阮一峯:JavaScript 運行機制詳解:再談Event Loop

前端小姐姐:完全搞懂瀏覽器Event-loop

小美娜娜:Eventloop不可怕,可怕的是趕上Promise

隆金岑:js事件循環機制(瀏覽器端Event Loop) 以及async/await的理解

相關文章
相關標籤/搜索