面試題
new Promise(resolve => { setTimeout(()=>{ console.log(666); new Promise(resolve => { resolve(); }) .then(() => {console.log(777);}) }) resolve(); }) .then(() => { new Promise(resolve => { resolve(); }) .then(() => {console.log(111);}) .then(() => {console.log(222);}); }) .then(() => { new Promise((resolve) => { resolve() }) .then(() => { new Promise((resolve) => { resolve() }) .then(() => {console.log(444)}) }) .then(() => { console.log(555); })}).then(() => { console.log(333);})
答案
111222333444555666777
如果你沒有得出正確的結果,有必要繼續往下看.
為了能正確解答上題,需要對宏任務、微任務以及Event-Loop深入理解.
知識點
宏任務
瀏覽器執行代碼的過程中,JS引擎會將大部分代碼進行分類,分別分到這兩個隊列中--宏任務(macrotask ) 和 微任務(microtask ) .
常見的宏任務:script(整體代碼), XHR回調,setTimeout, setInterval, setImmediate(node獨有), I/O.
上面的描述仍然有些生澀,下面借助案例深入理解.
app.js
setTimeout(()=>{ //宏任務2 console.log(2); },0) setTimeout(()=>{ //宏任務3 console.log(3); },0) console.log(1);
執行結果: 1 -- 2 -- 3
?瀏覽器開始運行 app.js 時啟動了第一個宏任務(宏任務1,指向app.js整體代碼)并開始執行.?在執行宏任務1途中遇到了第一個定時器,瀏覽器便會開啟一個新的宏任務2,定時器被添加到宏任務隊列等待,線程繼續往下執行.?隨后又遇到了定時器開啟一個新的宏任務3,定時器又被添加到宏任務隊列等待,宏任務3排在宏任務2的后面,線程繼續往下執行.?線程走到最后輸出了1,此時宏任務1就結束了.瀏覽器此刻就會去宏任務隊列中尋找,排在最前面的是宏任務2,發現延遲時間已到允許執行便輸出了2,宏任務2結束又執行宏任務3輸出3.
宏任務通常是由宿主環境開啟.比如在客戶端,瀏覽器就是宿主環境.開始執行一個腳本文件,開啟一個定時器任務以及ajax請求,都是瀏覽器在其底層完成,并非是通過js 引擎去做的這些工作.在服務器端,node就作為了宿主環境.
微任務
微任務是宏任務的組成部分,微任務與宏任務是包含關系,并非前后并列.如果要談微任務,需要指出它屬于哪個宏任務才有意義.
常見的宏任務:process.nextTick(nodejs端),Promise等.
app.js
console.log(1); new Promise((resolve)=>{ resolve(); }).then(()=>{ console.log(2) }) console.log(3)
執行結果: 1 -- 3 -- 2
?運行 app.js 腳本文件啟動宏任務1,第一行代碼執行輸出1.?碰到Promise,將then的回調函數放入宏任務1的微任務隊列中等待,線程繼續往下.?代碼跑到最后一行輸出3.此時同步代碼執行完畢,開始檢查當前宏任務中的微任務隊列.?運行微任務隊列中的第一個then回調函數輸出2.再檢查微任務隊列,沒有發現其他任務.?微任務隊列執行完畢,宏任務1執行完畢.
宏任務由宿主環境開啟,與此相對應,微任務是 js 引擎從代碼層面開啟的.
如果還對宏任務和微任務的關系模棱兩可,下面從 Event-Loop 角度詳細闡述.
Event-Loop

從上圖可知,宏任務形成了一個擁有先后順序的隊列.每個宏任務中分為同步代碼和微任務隊列.
?假設js當前的線程執行宏任務1,先執行宏任務1中的同步代碼.?如果碰到Promise或者process.nextTick,就把它們的回調放入當前宏任務1的微任務隊列中.?如果碰到setTimeout, setInterval之類就會在當前宏任務1的隊列后面開啟新的宏任務將回調放入其中.?同步代碼執行完,開始執行宏任務1的微任務隊列,直到微任務隊列的所有任務都執行完.?微任務隊列的所有任務執行完畢,宏任務1再看沒有其他代碼了,當前的事件循環結束.js線程開始執行下一個宏任務,直到所有宏任務執行完畢.如此整體便構成了事件循環機制.
延伸
dom操作屬于宏任務還是微任務
console.log(1); document.getElementById("div").style.color = "red"; console.log(2);
在實踐中發現,當上面代碼執行到第三行時,控制臺輸出了1并且頁面已經完成了重繪,div的顏色變成了紅色.
dom操作它既不是宏任務也不是微任務,它應該歸于同步執行的范疇.
requestAnimationFrame屬于宏任務還是微任務
setTimeout(() => { console.log("11111")}, 0)requestAnimationFrame(() => { console.log("22222")})new Promise(resolve => { console.log('promise'); resolve();}).then(() => {console.log('then')})
執行結果: promise -- then -- 22222 -- 11111
很多人會把 requestAnimationFrame 歸結到宏任務中,因為發現它會在微任務隊列完成后執行.
但實際上 requestAnimationFrame 它既不能算宏任務,也并非是微任務.它的執行時機是在當前宏任務范圍內,執行完同步代碼和微任務隊列后再執行.它仍然屬于宏任務范圍內,但是是在微任務隊列執行完畢后才執行.
Promise的運行機制
包裹函數是同步代碼
new Promise((resolve)=>{ console.log(1); resolve(); }).then(()=>{ console.log(2); })
new Promise里面的包裹的函數,也就是輸出1的那段代碼是同步執行的.而then包裹的函數才會被加載到微任務隊列中等待執行.
Promise鏈條如果沒有return
new Promise((resolve)=>{ console.log(1) resolve();}).then(()=>{ console.log(2);}).then(()=>{ console.log(3);}).then(()=>{ console.log(4);})
執行結果: 1 -- 2 -- 3 -- 4
在平時開發中,在Promise鏈中通常會返回一個新的Promise做異步操作返回相應的值.如下.
new Promise((resolve)=>{ console.log(1) resolve();}).then(()=>{ return new Promise((resolve)=>{ resolve(2) })}).then((n)=>{ console.log(n);})
執行結果: 1 -- 2
但上述代碼中,then函數的回調里沒有返回任何東西.但是后續then包含的回調函數仍然會依次執行,返回 1 -- 2 -- 3 -- 4.并且它可以在末尾無限接then函數,這些函數也都會依次執行.
多個then函數執行次序
new Promise((resolve)=>{ // 1 console.log("a") // 2 resolve(); // 3}).then(()=>{ // 4 console.log("b"); // 5}).then(()=>{ // 6 console.log("c"); // 7}) // 8console.log("d") // 9
執行結果: a -- d -- b -- c
?1,2,3行為同步執行的代碼,一氣呵成輸出 a.?此時線程走到第4行碰到then函數的回調,將其放入微任務的隊列等待.?線程繼續往后走直接跳到了第9行輸出了 d,為什么會忽略第6行的then直接跳到第9行呢?因為第4行的then函數回調執行完畢后才會開始執行第6行的代碼.(如果不理解為什么此刻會忽略掉第6行代碼可以查閱一下函數柯里化的概念).?同步代碼執行完畢,開始執行微任務隊列.此時微任務隊列里面只包含了一個then的回調函數,執行輸出b.?4,5行執行完畢后,開始執行第6行代碼.發現了then函數回調,將其放入微任務隊列中.此時第一個微任務執行完了,將其清空.?微任務隊列中還有一個剛放進去的微任務,執行輸出 c.清除此微任務,至此微任務隊列為空,全部任務執行完畢.
解題
有了以上知識的儲備再回到本文最初的面試題,這道題就可以輕松解決了.(為了方便闡述,加入右邊行號)
new Promise(resolve => { // 1 setTimeout(()=>{ // 2 console.log(666); // 3 new Promise(resolve => { // 4 resolve(); }) .then(() => {console.log(777);}) // 7 }) resolve(); // 9 }) // 10 .then(() => { // 11 new Promise(resolve => { // 12 resolve(); // 13 }) .then(() => {console.log(111);}) // 15 .then(() => {console.log(222);}); // 16 }) // 17 .then(() => { // 18 new Promise((resolve) => { // 19 resolve() }) .then(() => { // 22 new Promise((resolve) => { // 23 resolve() }) .then(() => {console.log(444)}) // 26 }) .then(() => { // 28 console.log(555); // 29 })}).then(() => { // 32 console.log(333);})
?線程執行第一行代碼,同步執行Promise包裹的函數.?在第二行發現定時器,啟動一個宏任務,將定時器的回調放入宏任務隊列等待,線程直接跳到第9行執行?第9行執行完開始執行第11行代碼發現then函數,放入當前微任務隊列中.線程往后再沒有可以執行的代碼了,于是開始執行微任務隊列.?執行微任務隊列進入第12行代碼,運行到第15行代碼時發現then函數放入微任務隊列等待.隨后線程直接跳到第18行,碰到then函數放到微隊列中.后續沒有可執行的代碼了,再開始執行微任務隊列的第一個任務也就是第15行代碼輸出111.?15行執行完執行到16行碰到then回調放入微任務隊列等待.隨后線程跳到18行的微任務開始執行,一直執行到22行碰到then函數又放入微任務隊列等待.此時線程繼續往下跳到第32行碰到then函數放入微任務隊列等待.后續沒有可執行的代碼了,再開始執行微任務隊列的第一個任務.?線程跳到第16行執行微任務輸出 222,隨后又跳到22行執行下一個微任務,在26行處碰到then函數放入微任務隊列等待.線程繼續執行下一個微任務跳到32行輸出 333.至此這一輪的三個微任務全部執行完畢清空,又開始執行微任務隊列的第一個任務,線程跳到第26行輸出 444.?線程執行到28行碰到then函數回調放入微任務隊列等待.后續沒有可執行的代碼了,再開始執行微任務隊列的第一個任務即29行代碼輸出 555.?所有微任務執行完畢,當前宏任務結束.線程開始執行下一個宏任務,線程跳到第三行輸出 666.?線程繼續往后第7行碰到then回調放入微任務隊列,后續沒有可執行的代碼了,再開始執行微任務隊列的第一個任務輸出 777.第二個宏任務執行完畢.
綜上所述:輸出分別為 111 -- 222 -- 333 -- 444 -- 555 -- 666 -- 777