當我在看節流函數的時候,碰到了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); 複製代碼
那麼這個節流函數是怎麼實現的節流呢?前端
讓咱們來看一下它的執行步驟(假設咱們一直不停的在滾動):node
window.onscroll = throttle(foo, 1000)
就會直接執行 throttle函數,定義了一個變量 sign
爲 true,而後碰到了 return 跳出 throttle函數,並返回另外一個匿名函數。那麼爲何在執行了 if判斷的過程當中,碰到了setTimeout,咱們的sign並無被改成true,從而一直的執行 if判斷呢?那麼就須要聊一聊js的運行機制了。終於要進正題了,真不容易...chrome
先看一下阮一峯大佬的segmentfault
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。promise
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。瀏覽器
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。bash
(4)主線程不斷重複上面的第三步。markdown
我本身歸類就是js中有:閉包
同步任務和異步任務
宏任務(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裏的版本4有很是詳細的解讀。
先看第一個代碼,思考一下答案
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"); }); async1() new Promise(function (resolve) { console.log("promise1"); resolve(); }).then(function () { console.log("promise2"); }); setImmediate(()=>{ console.log("setImmediate") }) process.nextTick(()=>{ console.log("process") }) console.log('script end'); 複製代碼
再看下面的代碼,思考一下答案,會是不同的嗎?
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') }, 1000) async1() new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('promise2') }) setImmediate(() => { console.log('setImmediate') }) process.nextTick(() => { console.log('process') }) console.log('script end') 複製代碼
先看答案: 第一個
script start
async1 start
async2
promise1
script end
process
async1 end
promise2
setTimeout
setImmediate
複製代碼
第二個
script start
async1 start
async2
promise1
script end
process
async1 end
promise2
setImmediate
setTimeout
複製代碼
阿勒?setTimeout和setImmediate順序竟然不同了。這是爲啥呢
由於 setImmediate 是在I/O回調只有當即執行。
對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的 進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調 若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了
我的測試了一下,setTimeout(fn, 2)的時候,也是先執行setTimeout,若是設置爲3ms或者以上的時候,會先執行setImmediate。
但當兩者在異步i/o callback內部調用時,老是先執行setImmediate,再執行setTimeout
const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }) // immediate // timeout 複製代碼
這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。
setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) }) }) // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1 複製代碼
阮一峯:JavaScript 運行機制詳解:再談Event Loop
小美娜娜:Eventloop不可怕,可怕的是趕上Promise