async 和 await 是怎麼工做的?——你不知道的生成器與協程

什麼是生成器函數?

生成器函數是一個帶星號的函數,能夠暫停執行與恢復執行。 async/await 使用了 協程(Generator) 和 微任務(Promise) 兩種技術來實現。promise

function* genDemo() {
  console.log("開始執行第 1 段");
  yield "generator 1";

  console.log("開始執行第 2 段");
  yield "generator 2";

  console.log("執行結束");
  return "generator 3";
}

console.log("main 0");
let gen = genDemo(); // 此處不會打印"開始執行第 1 段"只有在執行 gen.next 時纔會執行
console.log(gen.next().value); // 開始執行第 1 段 generator 1
console.log("main 1");
console.log(gen.next().value);
console.log("main 2");
console.log(gen.next().value);
console.log("main 3");
複製代碼
  • 輸出內容以下 main 0
    開始執行第 1 段
    generator 1
    main 1
    開始執行第 2 段
    generator 2
    main 2
    執行結束
    generator 3
    main 3

從上面輸出結果能夠看出,生成器函數與主函數是交替執行的。
生成器函數中遇到 yield 關鍵字時,就會返回 yield 後的內容給外部並把執行權交給外部函數去執行。
外部函數又能夠經過 gen.next 恢復生成器函數的執行。瀏覽器

什麼是協程?

協程是一種比線程更加輕量級的存在,能夠當作是跑在線程上的任務。就像一個進程能夠有多個線程同樣,一個線程也能夠有多個協程。
可是,線程上同時只能執行一個協程。好比:當前執行的是 A 協程,要啓動 B,就須要將主線程的控制權交給 B 協程;A 暫停執行,B 恢復執行。一般,若是從 A 協程啓動 B 協程,咱們就把 A 協程稱爲 B 協程的父協程。併發

協程執行流程圖

從圖中能夠看出:

  1. 經過生成器函數 genDemo 建立的協程 gen 建立以後並無當即執行。
  2. 經過調用 gen.next 可使協程執行。
  3. 經過 yield 關鍵字來暫停 gen 協程的執行,並返回主要信息給父協程。
  4. 若在執行期間遇到 return,JS 引擎會結束當前協程並將 return 後的內容返回給父協程。
  • 父協程與 gen 協程都有本身的調用棧,當控制權經過 yield 與 gen.next 互相切換時,V8 是如何切換調用棧的?
  1. gen 協程與父協程是在主線程上交互執行的,並非併發執行的,它們之間的切換是經過 yield 與 gen.next 配合完成。
  2. gen 中調用 yield 時,JS 引擎會保存 gen 協程當前的調用棧信息並恢復父協程的調用棧信息。同理,父協程中執行 gen.next 時,JS 引擎會保存父協程調用棧信息並恢復 gen 協程的調用棧信息。以下圖:
    協程間的切換

async/await

async 是什麼?

async 是一個經過異步執行隱式返回 Promise做爲結果的函數。異步

  • 隱式返回 Promise
async function foo() {
  return 2;
}
foo(); // Promise {<resolved>: 2}
複製代碼

await 是什麼?

觀察下面代碼的輸出:async

async function foo() {
  console.log(1);
  let a = await 100;
  console.log(a);
  console.log(2);
}
console.log(0);
foo();
console.log(3);
複製代碼

輸出:0 1 3 100 2 執行流程圖以下:
函數

async、await執行流程圖

當執行到 await 100 時,會建立一個 Promise 對象,以下:

let promise_ = new Promise((resolve, reject) => {
  resolve(100);
});
複製代碼

JS 引擎會將該任務提交到微任務隊列,而後暫停當前協程的執行,將主線程的控制權轉交給父協程執行,同時將 promise_ 對象返回給父協程(以下)。ui

async function foo() {
  ...
  let a = await 100
  ...
}
console.log(foo())
//Promise {<pending>}__proto__: ... "
// [[PromiseStatus]]: "resolved"
// [[PromiseValue]]: undefined

複製代碼

主線程控制權交給父協程後,父協程調用 promise_.then 來監控 promise 狀態的改變。spa

接下來執行父協程的流程,打印出 3。隨後父協程將執行結束,在結束前,進入微任務的檢查點去執行微任務隊列,微任務隊列中有 resolve(100) 等待執行,執行到這裏時,會觸發 promise_.then 中的回調函數,以下:線程

promise_.then(value => {
  // 回調函數觸發後,將主線程的控制權交給 foo 協程,並將 value 傳給協程
});
複製代碼

foo 協程激活後,將 value 的值給了變量 a,而後繼續執行後面語句,執行完成,將控制權歸還給父協程。code

思考題

async function foo() {
  console.log("foo");
}
async function bar() {
  console.log("bar start");
  await foo();
  console.log("bar end");
}
console.log("script start");
setTimeout(() => {
  console.log("setTimeout");
}, 0);
bar();
new Promise(resolve => {
  console.log("promise executor");
  resolve();
}).then(() => {
  console.log("promise then");
});
console.log("script end");
複製代碼

輸出以下:
scritp start
bar start foo
promise executor
script end
bar end
promise then
setTimeout

注意點: 第三步會輸出 foo,而不是 promise executor. 由於 await 是將 return 的值用 resolve 包裝提交到微任務隊列,console.log 語句不受影響,能夠直接輸出。

setTimeout 被放到延遲隊列中,而不是下一輪宏任務。
本輪宏任務執行完成後,會執行延遲隊列中的任務。
宏任務中父協程執行結束前,會去微任務隊列檢查執行微任務。

參考資料

瀏覽器工做原理與實踐

相關文章
相關標籤/搜索