每日技術:關於promise,async,setTimeout的執行順序

前端100問第10題

參考:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7 前端

 

請寫出下面代碼的運行結果git

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');

我當時寫的答案是:github

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. promise2
  7. async1 end
  8. setTimeout

正確答案是:promise

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

說明我仍是沒有真正理解它們的執行順序。因而看着大牛寫的答案來學習。瀏覽器

 

任務隊列

首先咱們須要明白如下幾件事情:異步

  • JS分爲同步任務和異步任務
  • 同步任務都在主線程上執行,造成一個執行棧
  • 主線程以外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件
  • 一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行

每一個任務都有一個任務源(task source),源自同一個任務源的task必須放到同一個任務隊列,從不一樣源來的則被添加到不一樣隊列。setTimeout/Promise等API就是任務源。async

 

宏任務 (macro task)

每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)函數

瀏覽器爲了可以使得JS內部macro task與DOM任務可以有序的執行,會在一個macro task執行結束後,在下一個macro task執行開始前,對頁面進行從新渲染。流程以下:post

(macro)task -> 渲染 -> (macro)task ->...學習

(macro)task主要包含:script總體代碼、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js環境)

微任務 (microtask)

在當前task執行結束後當即執行的任務。也就是說,在當前task任務後,下一個task以前,在渲染以前

因此它的響應速度相比setTimeout會更快,由於無需等渲染。也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)

microtask主要包含:Promise.then、MutationObserver、process.nextTick(Node.js環境)

 

運行機制

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

 

寫在Promise中的代碼被當作同步任務當即執行。而在async/await中,在出現await以前,其中的代碼也是當即執行。

await是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,而後就會跳出整個async函數來執行後面的代碼。

await後面的代碼是microtask

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')      
}

等價於

async function async1() {
  console.log('async1 start')
  Promise.resolve(async2()).then(() => {
    console.log('async1 end')
  })    
}    

 

以上就是本道題涉及到的全部相關知識點了。下面咱們再回到這道題來一步一步看看怎麼回事

 

1.首先,事件循環從宏任務隊列開始,這個時候,宏任務隊列中,只有一個script總體代碼任務,當遇到任務源時,則會先分發任務到對應的任務隊列中去。

2.而後咱們看到首先定義了兩個async函數,而後遇到了console語句,直接輸出script start.輸出以後,script任務繼續往下執行,遇到setTimeout,其做爲一個宏任務源,則會先將其任務分發到對應的隊列中

3.script任務繼續往下執行,執行了async1()函數,輸出async1 start,遇到await時,會將await後面的表達式執行一遍,因此緊接着輸出async2,而後將await後面的代碼也就是console.log('async1 end')加入到microtask中的Promise隊列中。接着跳出async1函數來執行後面的代碼

4.接着遇到Promise實例。因爲Promise中的函數時當即執行的,然後續的.then則會被分發到microtask的Promise隊列中。因此會先輸出promise1, 而後執行resolve,將promise2分配到對應隊列。

5.最後只有一句輸出了script end,至此,全局任務就執行完畢了。

根據上述,每次執行完一個宏任務以後,回去檢查是否存在Microtask,若是有,則執行Microtasks直至清空Microtask Queue

於是再script任務執行完畢以後,開始查找清空微任務隊列。此時,微任務中,Promise隊列有的兩個任務async1 end和promise2,所以按前後順序輸出。當全部的Microtasks執行完畢以後,表示第一輪的循環就結束了。

6.第二輪循環依舊從宏任務隊列開始,此時宏任務中只有一個setTimeout,取出直接輸出便可,至此整個流程結束。

 

最後原答案做者又加了三個變式用來加深印象,很是好。

 

變式一

在第一個變式中將async2中的函數也變成Promise函數,代碼以下:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2作出以下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

這一次我寫的答案和做者給出的答案同樣,以下:

  1. script start
  2. async1 start
  3. promise1
  4. promise3
  5. script end
  6. promise2
  7. async1 end
  8. promise4
  9. setTimeout

 

變式二

async function async1() {
    console.log('async1 start');
    await async2();
    //更改以下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改以下:
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

這道題我給出的答案是:

  1. script start
  2. async1 start
  3. promise1
  4. script end
  5. promise2
  6. setTimeout2
  7. setTimeout1
  8. setTimeout3

正確答案是:

  1. script start
  2. async1 start
  3. promise1
  4. script end
  5. promise2
  6. setTimeout3
  7. setTimeout2
  8. setTimeout1

 

在輸出爲promise2以後,接下來會按照加入setTimeout隊列的順序來依次輸出,經過代碼咱們能夠看到加入順序爲3 2 1,因此會按3,2,1的順序來輸出。

 

變式三

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

答案:

  1. script start
  2. a1 start
  3. a2
  4. promise2
  5. script end
  6. promise1
  7. a1 end
  8. promise2.then
  9. promise3
  10. setTimeout
相關文章
相關標籤/搜索