當我在看節流函數的時候,碰到了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
window.onscroll = throttle(foo, 1000)
就會直接執行 throttle函數,定義了一個變量 sign
爲 true,而後碰到了 return 跳出 throttle函數,並返回另外一個匿名函數。那麼爲何在執行了 if判斷的過程當中,碰到了setTimeout,咱們的sign並無被改成true,從而一直的執行 if判斷呢?那麼就須要聊一聊js的運行機制了。終於要進正題了,真不容易...segmentfault
先看一下阮一峯大佬的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(事件循環)。此機制具體以下:主線程會不斷從任務隊列中按順序取任務執行,每執行完一個任務都會檢查microtask隊列是否爲空(執行完一個任務的具體標誌是函數執行棧爲空),若是不爲空則會一次性執行完全部microtask。而後再進入下一個循環去任務隊列中取下一個任務執行。
我又給總結了一下籠統的過程:script(宏任務) - 清空微任務隊列 - 執行一個宏任務 - 清空微任務隊列 - 執行一個宏任務, 如此往復。
要作到心中有隊列,有先進先出的概念
借用前端小姐姐的一張圖來解釋:
如今再看開頭的節流函數,就明白爲何碰到了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');
});
複製代碼
根據前面的流程
Promise1
。setTimeout1
。又發現了 一個 setTimeout,放進任務隊列。看見了 Promise.then() ,打印setTimeout 裏的 Promise
。setTimeout2
。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
。
截至到上一關,我本覺得我已經徹底掌握了event-loop。後來我看到了 async/await , async await是generator
和 Promise
的語法糖這個你們應該都知道,可是打印以後跟我預期的不太同樣,頓時有點兒蒙圈,後來一分析,原來如此。
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以後執行的函數。
如此一來,就能夠得出上面的結果了。
可是,你也許打印出來會是下面這樣的結果:
這個就跟V8有關係了(在chrome 71版本中,我打印出的是圖片中的結果)。至於async/await和promise到底誰會先執行,這裏偷個懶,你們看 小美娜娜:Eventloop不可怕,可怕的是趕上Promise裏的版本5有很是詳細的解讀。
阮一峯:JavaScript 運行機制詳解:再談Event Loop