有關Eventloop+Promise的面試題大約分如下幾個版本——駕輕就熟版、遊刃有餘版、爐火純青版、登峯造極版和究極變態版。假設小夥伴們戰到最後一題,之後遇到此類問題,都是所向披靡。固然若是面試官們還能想出更變態的版本,算我輸。node
考點:eventloop中的執行順序,宏任務微任務的區別。吐槽:這個不懂,沒得救了,回家從新學習吧。react
setTimeout(()=>{ console.log(1) },0) Promise.resolve().then(()=>{ console.log(2) }) console.log(3)
這個版本的面試官們就特別友善,僅僅考你一個概念理解,瞭解宏任務(marcotask)微任務(microtask),這題就是送分題。git
筆者答案:這個是屬於Eventloop的問題。main script運行結束後,會有微任務隊列和宏任務隊列。微任務先執行,以後是宏任務。
PS:概念問題github
有時候會有版本是宏任務>微任務>宏任務,在這裏筆者須要講清楚一個概念,以避免混淆。這裏有個main script的概念,就是一開始執行的代碼(代碼總要有開始執行的時候對吧,否則宏任務和微任務的隊列哪裏來的),這裏被定義爲了宏任務(筆者喜歡將main script的概念單獨拎出來,不和兩個任務隊列混在一塊兒),而後根據main script中產生的微任務隊列和宏任務隊列,分別清空,這個時候是先清空微任務的隊列,再去清空宏任務的隊列。面試
這一個版本,面試官們爲了考驗一下對於Promise的理解,會給題目加點料:segmentfault
考點:Promise的executor以及then的執行方式吐槽:這是個小坑,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(4)
此題看似在考Eventloop,實則考的是對於Promise的掌握程度。Promise的then是微任務你們都懂,可是這個then的執行方式是如何的呢,以及Promise的executor是異步的仍是同步的?promise
錯誤示範:Promise的then是一個異步的過程,每一個then執行完畢以後,就是一個新的循環的,因此第二個then會在setTimeout以後執行。(沒錯,這就是某年某月某日筆者的一個回答。請給我一把槍,真想打死當時的本身。)瀏覽器
正確示範:這個要從Promise的實現來講,Promise的executor是一個同步函數,即非異步,當即執行的一個函數,所以他應該是和當前的任務一塊兒執行的。而Promise的鏈式調用then,每次都會在內部生成一個新的Promise,而後執行then,在執行的過程當中不斷向微任務(microtask)推入新的函數,所以直至微任務(microtask)的隊列清空後纔會執行下一波的macrotask。
(若是你們不嫌棄,能夠參考個人另外一篇文章,從零實現一個Promise,裏面的解釋淺顯易懂。)
咱們以babel的core-js中的promise實現爲例,看一眼promise的執行規範:babel
代碼位置:promise-polyfill
PromiseConstructor = function Promise(executor) { //... try { executor(bind(internalResolve, this, state), bind(internalReject, this, state)); } catch (err) { internalReject(this, state, err); } };
這裏能夠很清除地看到Promise中的executor是一個當即執行的函數。
then: function then(onFulfilled, onRejected) { var state = getInternalPromiseState(this); var reaction = newPromiseCapability(speciesConstructor(this, PromiseConstructor)); reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true; reaction.fail = typeof onRejected == 'function' && onRejected; reaction.domain = IS_NODE ? process.domain : undefined; state.parent = true; state.reactions.push(reaction); if (state.state != PENDING) notify(this, state, false); return reaction.promise; },
接着是Promise的then函數,很清晰地看到reaction.promise
,也就是每次then執行完畢後會返回一個新的Promise。也就是當前的微任務(microtask)隊列清空了,可是以後又開始添加了,直至微任務(microtask)隊列清空纔會執行下一波宏任務(marcotask)。
//state.reactions就是每次then傳入的函數 var chain = state.reactions; microtask(function () { var value = state.value; var ok = state.state == FULFILLED; var i = 0; var run = function (reaction) { //... }; while (chain.length > i) run(chain[i++]); //... });
最後是Promise的任務resolve以後,開始執行then,能夠看到此時會批量執行then中的函數,並且還給這些then中回調函數放入了一個microtask這個很顯眼的函數之中,表示這些回調函數是在微任務中執行的。
那麼在沒有Promise的瀏覽器中,微任務這個隊列是如何實現的呢?
小知識: babel中對於微任務的polyfill,若是是擁有setImmediate
函數平臺,則使用之,若沒有則自定義則利用各類好比nodejs中的process.nextTick
,瀏覽器中支持postMessage
的,或者是經過create一個script來實現微任務(microtask)。最終的最終,是使用setTimeout,不過這個就和微任務無關了,promise變成了宏任務的一員。
拓展思考:
爲何有時候,then中的函數是一個數組?有時候就是一個函數?
咱們稍稍修改一下上述題目,將鏈式調用的函數,變成下方的,分別調用then。且不說這和鏈式調用之間的不一樣用法,這邊只從實踐角度辨別二者的不一樣。鏈式調用是每次都生成一個新的Promise,也就是說每一個then中回調方法屬於一個microtask,而這種分別調用,會將then中的回調函數push到一個數組之中,而後批量執行。再換句話說,鏈式調用可能會被Evenloop中其餘的函數插隊,而分別調用則不會(僅針對最普通的狀況,then中無其餘異步操做。)。
let a=new Promise((resolve)=>{ console.log(2) resolve() }) a.then(()=>{ console.log(3) }) a.then(()=>{ console.log(4) })
下一模塊會對此微任務(microtask)中的「插隊」行爲進行詳解。
這一個版本是上一個版本的進化版本,上一個版本的promise的then函數並未返回一個promise,若是在promise的then中建立一個promise,那麼結果該如何呢?
考點:promise的進階用法,對於then中return一個promise的掌握吐槽: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") })
按照上一節最後一個microtask的實現過程,也就是說一個Promise全部的then的回調函數是在一個microtask函數中執行的,可是每個回調函數的執行,又按照狀況分爲當即執行,微任務(microtask)和宏任務(macrotask)。
遇到這種嵌套式的Promise不要慌,首先要心中有一個隊列,可以將這些函數放到相對應的隊列之中。
Ready GO
第一輪
[promise1]
第二輪
then11
以及新promise2的promise2
第三輪
then21
和promise1的第二個then函數輸出then12
。第四輪
then23
END
最終結果[promise1,then11,promise2,then21,then12,then23]
。
變異版本1:若是說這邊的Promise中then返回一個Promise呢??
new Promise((resolve,reject)=>{ console.log("promise1") resolve() }).then(()=>{ console.log("then11") return new Promise((resolve,reject)=>{ console.log("promise2") resolve() }).then(()=>{ console.log("then21") }).then(()=>{ console.log("then23") }) }).then(()=>{ console.log("then12") })
這裏就是Promise中的then返回一個promise的情況了,這個考的重點在於Promise而非Eventloop了。這裏就很好理解爲什麼then12
會在then23
以後執行,這裏Promise的第二個then至關因而掛在新Promise的最後一個then的返回值上。
變異版本2:若是說這邊不止一個Promise呢,再加一個new 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") }) new Promise((resolve,reject)=>{ console.log("promise3") resolve() }).then(()=>{ console.log("then31") })
笑容逐漸變態,一樣這個咱們能夠本身心中排一個隊列:
第一輪
promise2的第一個then
,promise3的第一個then
]第二輪
promise2的第一個then
,promise1的第二個then
]第三輪
promise2的第二個then
]第四輪
最終輸出:[promise1
,promise3
,then11
,promise2
,then31
,then21
,then12
,then23
]
考點:在async/await之下,對Eventloop的影響。槽點:別被async/await給騙了,這題不難。
相信你們也看到過此類的題目,我這裏有個至關簡易的解釋,不知你們是否有興趣。
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');
async/await僅僅影響的是函數內的執行,而不會影響到函數體外的執行順序。也就是說async1()並不會阻塞後續程序的執行,await async2()
至關於一個Promise,console.log("async1 end");
至關於前方Promise的then以後執行的函數。
按照上章節的解法,最終輸出結果:[script start
,async1 start
,async2
,promise1
,script end
,async1 end
,promise2
,settimeout
]
若是瞭解async/await的用法,則並不會以爲這題是困難的,但如果不瞭解或者只知其一;不知其二,那麼這題就是災難啊。
async function async1() { console.log("async1 start"); await async2(); console.log("async1 end"); } async function async2() { console.log( 'async2'); } // 用於test的promise,看看await究竟在什麼時候執行 new Promise(function (resolve) { console.log("promise1"); resolve(); }).then(function () { console.log("promise2"); }).then(function () { console.log("promise3"); }).then(function () { console.log("promise4"); }).then(function () { console.log("promise5"); });
先給你們出個題,若是讓你polyfill一下async/await,你們會怎麼polyfill上述代碼?下方先給出筆者的版本:
function promise1(){ return new Promise((resolve)=>{ console.log("async1 start"); promise2().then(()=>{ console.log("async1 end"); resolve() }) }) } function promise2(){ return new Promise((resolve)=>{ console.log( 'async2'); resolve() }) }
在筆者看來,async
自己是一個Promise
,而後await
確定也跟着一個Promise
,那麼新建兩個function,各自返回一個Promise。接着function promise1
中須要等待function promise2
中Promise完成後才執行,那麼就then
一下咯~。
根據這個版本得出的結果:[async1 start,async2,promise1,async1 end,promise2,...]
,async的await在test的promise.then以前,其實也可以從筆者的polifill中得出這個結果。
而後讓筆者驚訝的是用原生的async/await,得出的結果與上述polyfill不一致!得出的結果是:[async1 start,async2,promise1,promise2,promise3,async1 end,...]
,因爲promise.then每次都是一輪新的microtask,因此async是在2輪microtask以後,第三輪microtask才得以輸出(關於then請看版本三的解釋)。
/ 突如其來的沉默 /
這裏插播一條,async/await由於要通過3輪的microtask才能完成await,被認爲開銷很大,所以以後V8和Nodejs12開始對此進行了修復,詳情能夠看 github上面這一條pull
那麼,筆者換一種方式來polyfill,相信你們都已經充分了解await後面是一個Promise,可是假設這個Promise不是好Promise怎麼辦?異步是好異步,Promise不是好Promise。V8就很兇殘,加了額外兩個Promise用於解決這個問題,簡化了下源碼,大概是下面這個樣子:
// 不太準確的一個描述 function promise1(){ console.log("async1 start"); // 暗中存在的promise,筆者認爲是爲了保證async返回的是一個promise const implicit_promise=Promise.resolve() // 包含了await的promise,這裏直接執行promise2,爲了保證promise2的executor是同步的感受 const promise=promise2() // https://tc39.github.io/ecma262/#sec-performpromisethen // 25.6.5.4.1 // throwaway,爲了規範而存在的,爲了保證執行的promise是一個promise const throwaway= Promise.resolve() //console.log(throwaway.then((d)=>{console.log(d)})) return implicit_promise.then(()=>{ throwaway.then(()=>{ promise.then(()=>{ console.log('async1 end'); }) }) }) }
ps:爲了強行推遲兩個microtask執行,筆者也是煞費苦心。
總結一下:async/await有時候會推遲兩輪microtask,在第三輪microtask執行,主要緣由是瀏覽器對於此方法的一個解析,因爲爲了解析一個await,要額外建立兩個promise,所以消耗很大。後來V8爲了下降損耗,因此剔除了一個Promise,而且減小了2輪microtask,因此如今最新版本的應該是「零成本」的一個異步。
饕餮大餐,什麼變態的內容都往裏面加,想一想就很豐盛。能考到這份上,只能說面試官人狠話也多。
考點:nodejs事件+Promise+async/await+佛系setImmediate槽點:筆者都不知道那個可能先出現
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');
隊列執行start
第一輪:
async
,promise.then
,process
]setTimeout
,setImmediate
]第二輪
setTimeout
,setImmediate
]第三輪
最終結果:[script start
,async1 start
,async2
,promise1
,script end
,process
,async1 end
,promise2
,setTimeout
,setImmediate
]
一樣"async1 end","promise2"之間的優先級,因平臺而異。
在處理一段evenloop執行順序的時候:
第一步確認宏任務,微任務
最後一步記住一些特別事件
process.nextTick
優先級高於Promise.then
有關V8中如何實現async/await的,更快的異步函數和 Promise
有關async/await規範的,ecma262
還有babel-polyfill的源碼,promise
Hello~Anybody here?
原本筆者是不想寫這篇文章的,由於有種5年高考3年模擬的既視感,奈何面試官們都太兇殘了,爲了「折磨」面試者無所不用其極,怎麼變態怎麼來。不過所以筆者算是完全掌握了Eventloop的用法,塞翁失馬吧~
有小夥伴看到最後嘛?來和筆者聊聊你遇到過的的Eventloop+Promise的變態題目。
歡迎轉載~但請註明出處~首發於掘金~Eventloop不可怕,可怕的是趕上Promise
題外話:來segmentfault試水~啊哈哈哈啊哈哈