摘要:就是每次傳入的函數最后是的任務之后,開始執行,可以看到此時會批量執行中的函數,而且還給這些中回調函數放入了一個這個很顯眼的函數之中,表示這些回調函數是在微任務中執行的。下一模塊會對此微任務中的插隊行為進行詳解。
有關Eventloop+Promise的面試題大約分以下幾個版本——得心應手版、游刃有余版、爐火純青版、登峰造極版和究極{{BANNED}}版。假設小伙伴們戰到最后一題,以后遇到此類問題,都是所向披靡。當然如果面試官們還能想出更{{BANNED}}的版本,算我輸。
版本一:得心應手版考點:eventloop中的執行順序,宏任務微任務的區別。吐槽:這個不懂,沒得救了,回家重新學習吧。
setTimeout(()=>{
console.log(1)
},0)
Promise.resolve().then(()=>{
console.log(2)
})
console.log(3)
這個版本的面試官們就特別友善,僅僅考你一個概念理解,了解宏任務(marcotask)微任務(microtask),這題就是送分題。
筆者答案:這個是屬于Eventloop的問題。main script運行結束后,會有微任務隊列和宏任務隊列。微任務先執行,之后是宏任務。
PS:概念問題
有時候會有版本是宏任務>微任務>宏任務,在這里筆者需要講清楚一個概念,以免混淆。這里有個main script的概念,就是一開始執行的代碼(代碼總要有開始執行的時候對吧,不然宏任務和微任務的隊列哪里來的),這里被定義為了宏任務(筆者喜歡將main script的概念多帶帶拎出來,不和兩個任務隊列混在一起),然后根據main script中產生的微任務隊列和宏任務隊列,分別清空,這個時候是先清空微任務的隊列,再去清空宏任務的隊列。
版本二:游刃有余版這一個版本,面試官們為了考驗一下對于Promise的理解,會給題目加點料:
考點: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的then是一個異步的過程,每個then執行完畢之后,就是一個新的循環的,所以第二個then會在setTimeout之后執行。(沒錯,這就是某年某月某日筆者的一個回答。請給我一把槍,真想打死當時的自己。)
正確示范:這個要從Promise的實現來說,Promise的executor是一個同步函數,即非異步,立即執行的一個函數,因此他應該是和當前的任務一起執行的。而Promise的鏈式調用then,每次都會在內部生成一個新的Promise,然后執行then,在執行的過程中不斷向微任務(microtask)推入新的函數,因此直至微任務(microtask)的隊列清空后才會執行下一波的macrotask。詳細解析
(如果大家不嫌棄,可以參考我的另一篇文章,從零實現一個Promise,里面的解釋淺顯易懂。)
我們以babel的core-js中的promise實現為例,看一眼promise的執行規范:
代碼位置: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
第一輪
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
最終結果[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")
})
笑容逐漸{{BANNED}},同樣這個我們可以自己心中排一個隊列:
第一輪
current task: promise1,promise3
micro task queue: [promise2的第一個then,promise3的第一個then]
第二輪
current task: then11,promise2,then31
micro task queue: [promise2的第一個then,promise1的第二個then]
第三輪
current task: then21,then12
micro task queue: [promise2的第二個then]
第四輪
current task: then23
micro task queue: []
最終輸出:[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的then和promise的then的優先級的問題,請看下方詳解。*
async/await與promise的優先級詳解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,所以現在最新版本的應該是“零成本”的一個異步。版本五:究極{{BANNED}}版
饕餮大餐,什么{{BANNED}}的內容都往里面加,想想就很豐盛。能考到這份上,只能說面試官人狠話也多。
考點: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
第一輪:
current task:"script start","async1 start","async2","promise1",“script end”
micro task queue:[async,promise.then,process]
macro task queue:[setTimeout,setImmediate]
第二輪
current task:process,async1 end ,promise.then
micro task queue:[]
macro task queue:[setTimeout,setImmediate]
第三輪
current task:setTimeout,setImmediate
micro task queue:[]
macro task queue:[]
最終結果:[script start,async1 start,async2,promise1,script end,process,async1 end,promise2,setTimeout,setImmediate]
同樣"async1 end","promise2"之間的優先級,因平臺而異。
筆者干貨總結在處理一段evenloop執行順序的時候:
第一步確認宏任務,微任務
宏任務:script,setTimeout,setImmediate,promise中的executor
微任務:promise.then,process.nextTick
第二步解析“攔路虎”,出現async/await不要慌,他們只在標記的函數中能夠作威作福,出了這個函數還是跟著大部隊的潮流。
第三步,根據Promise中then使用方式的不同做出不同的判斷,是鏈式還是分別調用。
最后一步記住一些特別事件
比如,process.nextTick優先級高于Promise.then
參考網址,推薦閱讀:有關V8中如何實現async/await的,更快的異步函數和 Promise
有關async/await規范的,ecma262
還有babel-polyfill的源碼,promise
后記Hello~Anybody here?
本來筆者是不想寫這篇文章的,因為有種5年高考3年模擬的既視感,奈何面試官們都太兇殘了,為了“折磨”面試者無所不用其極,怎么{{BANNED}}怎么來。不過因此筆者算是徹底掌握了Eventloop的用法,因禍得福吧~
有小伙伴看到最后嘛?來和筆者聊聊你遇到過的的Eventloop+Promise的{{BANNED}}題目。
歡迎轉載~但請注明出處~首發于掘金~Eventloop不可怕,可怕的是遇上Promise
題外話:來segmentfault試水~啊哈哈哈啊哈哈
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.hztianpu.com/yun/103054.html
摘要:記錄下我遇到的面試題,都有大佬分享過,附上各個大佬的文章,總結出其中的主要思想即可。推薦黑金團隊的文章前端緩存最佳實踐推薦名揚的文章淺解強緩存和協商緩存狀態碼重點是等,要給面試官介紹清楚。前言 在這互聯網的寒冬臘月時期,雖說過了金三銀四,但依舊在招人不斷。更偏向于招聘高級開發工程師。本人在這期間求職,去了幾家創業,小廠,大廠廝殺了一番,也得到了自己滿意的offer。 整理一下自己還記得的面試...
摘要:學習開發,無論是前端開發還是都避免不了要接觸異步編程這個問題就和其它大多數以多線程同步為主的編程語言不同的主要設計是單線程異步模型。由于異步編程可以實現非阻塞的調用效果,引入異步編程自然就是順理成章的事情了。 學習js開發,無論是前端開發還是node.js,都避免不了要接觸異步編程這個問題,就和其它大多數以多線程同步為主的編程語言不同,js的主要設計是單線程異步模型。正因為js天生的與...
摘要:本周精讀內容是逃離地獄。精讀仔細思考為什么會被濫用,筆者認為是它的功能比較反直覺導致的。同時,筆者認為,也不要過渡利用新特性修復新特性帶來的問題,這樣反而導致代碼可讀性下降。 本周精讀內容是 《逃離 async/await 地獄》。 1 引言 終于,async/await 也被吐槽了。Aditya Agarwal 認為 async/await 語法讓我們陷入了新的麻煩之中。 其實,筆者...
摘要:當然是否需要培訓這個話題,得基于兩個方面,如果你是計算機專業畢業的,大學基礎課程學的還可以,我建議不需要去培訓,既然有一定的基礎,那就把去培訓浪費的四個月,用去實習,培訓是花錢,實習是掙錢,即使工資低點,一正一負自己算算吧。 上周一篇《程序員平時該如何學習來提高自己的技術》火了之后,「非著名程序員」微信公眾號的后臺經常收到程序員和一些初學者的消息,問一些技術提高的問題,而且又恰逢畢業季...
閱讀 3634·2021-11-19 09:40
閱讀 1573·2021-10-11 11:07
閱讀 5176·2021-09-22 15:07
閱讀 3087·2021-09-02 15:15
閱讀 2237·2019-08-30 15:55
閱讀 664·2019-08-30 15:43
閱讀 1071·2019-08-30 11:13
閱讀 1628·2019-08-29 15:36