異步編程的終極解決方案 async-await:用同步的方式去寫異步代碼

早期的回調函數

回調函數我們經常有寫到,比如:

ajax(url, (res) ={
  console.log(res);
})
複製代碼

但是這種回調函數有一個大缺陷,就是會寫出 回調地獄(Callback hell

比如,如果多個回調存在依賴,可能會寫成:

ajax(url, (res) ={
  console.log(res);
  // ...處理代碼
  ajax(url2, (res2) ={
    console.log(res2);
    // ...處理代碼
    ajax(url3, (res3) ={
      console.log(res3);
      // ...處理代碼
    })
  })
})
複製代碼

這個就是 回調地獄

早期回調函數的優缺點:

過渡方案 Generator

ES6 新引入了 Generator 函數(生成器函數),可以通過 yield 關鍵字,把函數的執行流掛起,爲改變執行流程提供了可能,從而爲異步編程提供解決方案。最大的特點就是 可以控制函數的執行

Generator 有兩個區分於普通函數的部分:

Generator 函數的具體使用方式是:

function* fn() {
  console.log("one");
  yield '1';
  console.log("two");
  yield '2';
  console.log("three");
  return '3';
}
複製代碼

調用 Generator 函數和調用普通函數一樣,在函數名後面加上 () 即可,但是 Generator 函數不會像普通函數一樣立即執行,而是 返回一個指向內部狀態對象的指針,所以要調用遍歷器對象 Iterator 的 next 方法,指針就會從函數頭部或者上一次停下來的地方開始執行

如下:

next 方法:

一般情況下, next 方法不傳入參數的時候,yield 表達式的返回值是 undefined。當 next 傳入參數的時候,該參數會作爲上一步 yield 的返回值。

Generator 生成器也是通過同步的方式寫異步代碼的,也可以解決回調地獄的問題,但是比較難以理解,希望下面的例子能夠幫助你理解 Generator 生成器:

function* sum(a) {
  console.log('a:', a);
  let b = yield 1;
  console.log('b:', b);
  let c = yield 2;
  console.log('c:', c);
  let sum = a + b + c;
  console.log('sum:', sum)
  return sum;
}
複製代碼

如下圖:

如下圖:

協程

我們知道,async/await 是一個自動執行的 Generator 函數,上面已經介紹了 Generator 函數,那麼接下來很有必要介紹一下 V8 引擎是如何實現一個函數的暫停和恢復 的呢?

要搞懂函數爲何能暫停和恢復,首先要了解 協程 的概念。進程和線程我們都知道,那麼協程是什麼呢?

協程是一種比線程更加輕量級的存在。可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程,比如當前執行的是 A 協程,要啓動 B 協程,那麼 A 協程就需要將主線程的控制權交給 B 協程,這就體現在 A 協程暫停執行,B 協程恢復執行;同樣,也可以從 B 協程中啓動 A 協程。通常,如果從 A 協程啓動 B 協程,我們就把 A 協程稱爲 B 協程的父協程

正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。最重要的是,協程不是被操作系統內核所管理,而是完全由程序所控制(即在用戶態執行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。

可以結合代碼理解:

function* genDemo() {
  console.log("開始執行第一段")
  yield 'generator 2'

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

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

  console.log("執行結束")
  return 'generator 2'
}

console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')
複製代碼

執行過程如下圖所示,可以重點關注協程之間的切換:

從圖中可以看出來協程的四點規則:

協程之間的切換:

其實在 JS 中,Generator 生成器就是協程的一種實現方式。

成熟方案 Promise

關於 Promise,可以去看我上一篇文章:《異步編程 Promise:從使用到手寫實現(4200 字長文)》,在這一篇文章中詳細介紹了 Promise 如何解決回調地獄的問題,瞭解 Promise 和微任務的淵源,然後帶你一步一步的解構手寫實現一個簡單的 Promise,最後簡單介紹並手寫實現了一些 Promise 的 API,包括 Promise.allPromise.allSettledPromise.racePromise.finally 等 API。

終極解決方案 async/await

使用 Promise 能很好地解決回調地獄的問題,但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較複雜的話,那麼整段代碼將充斥着 then,語義化不明顯,代碼不能很好地表示執行流程。

基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了 在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰。

其實 async/await 技術背後的祕密就是 Promise 和 Generator 生成器應用,往低層說就是 微任務和協程應用。要搞清楚 async 和 await 的工作原理,我們得對 async 和 await 分開分析。

async

async 到底是什麼?根據 MDN 定義,async 是一個通過 異步執行並隱式返回 Promise 作爲結果的函數。重點關注兩個詞:異步執行和隱式返回 Promise

先來看看是如何隱式返回 Promise 的,參考下面的代碼:

async function async1() {
  return '秀兒';
}
console.log(async1()); // Promise {<fulfilled>: "秀兒"}
複製代碼

執行這段代碼,可以看到調用 async 聲明的 async1 函數返回了一個 Promise 對象,狀態是 resolved,返回結果如下所示:Promise {<fulfilled>: "秀兒"}。和 Promise 的鏈式調用 then 中處理返回值一樣。

await

await 需要跟 async 搭配使用,結合下面這段代碼來看看 await 到底是什麼:

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

站在 協程 的視角來看看這段代碼的整體執行流程圖:

結合上圖來分析 async/await 的執行流程:

promise_.then((value) ={
  // 回調函數被激活後
  // 將主線程控制權交給foo協程,並將vaule值傳給協程
})
複製代碼

以上就是 await/async 的執行流程。正是因爲 async 和 await 在背後做了大量的工作,所以我們才能用同步的方式寫出異步代碼來。

當然也存在一些缺點,因爲 await 將異步代碼改造成了同步代碼,如果多個異步代碼沒有依賴性卻使用了 await 會導致性能上的降低。

async/await總結

異步編程總結

關於本文

來源:起風了 Q

https://juejin.cn/post/6978689182809997320

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/V8wdoaIzODnvWiwk2ylXuQ