【譯】Async-Await≈Generators+Promises

zhCN_超發表於2018-05-23

Async-Await ≈ Generators + Promises

這篇文章我將介紹ES2017async函式為什麼是ES2016GeneratorsPromises特性功能的語法糖。

閱讀須知

  • 本文不對三者概念進行介紹和講解
  • 本文唯一的目的就是介紹如何利用GeneratorsPromises去實現async
  • 本文對async和其他相似實現不進行優劣評價
  • 本文程式碼都是經過巧妙設計以便於理解,他們不適用於實際開發

為什麼?

既然async函式已被原生支援,還有理解它工作原理的必要嗎?

呃,除了因為好奇它的原理之外,更重要的是為了去支援舊的執行平臺。如果你希望使用了新功能的程式碼可以執行在舊的瀏覽器版本和Node.js版本,你可能需要使用諸如Babel這樣的工具去轉換這些新特性。

因此,深刻理解async函式如何被分解成generatorspromises後,在你閱讀或除錯轉換後的程式碼能派上很大用場。比如,這是一個簡單的async函式:

【譯】Async-Await≈Generators+Promises

Babel轉換成ES2016程式碼如下(不用完全看懂,下文會解釋):

【譯】Async-Await≈Generators+Promises

兩者差異很大!當然,如果你理解了async的工作原理,那麼這段轉換之後的程式碼對你來說也是小菜一碟。

另一個有趣的事實是,瀏覽器也會將async函式進行實現:瀏覽器像Babel一樣利用generatorspromises轉換async

那麼到底發生了些什麼?

有些時候,為了理解一些東西如何運作,最好的方法就是自己動手做。

比如我們有一段使用了async函式的程式碼片段,我們如何利用generatorspromises去重寫它呢?

這是我們的async函式:

【譯】Async-Await≈Generators+Promises

函式體中依次執行三個非同步任務,每個任務依賴前一個任務的完成。最後,函式返回最後一個任務的結果。

如何使用generators重寫

生成器的功能是:可以退出並再次進入。讓我們快速回顧一下它的工作方式,以下是一個簡單的generator函式:

【譯】Async-Await≈Generators+Promises

這個生成器函式gen擁有一些有趣的特性(從MDN摘取):

  1. 當一個generator函式被呼叫,函式體內程式碼並不立即執行。它返回一個遵循了迭代器協議迭代器物件:它有next方法
  2. 執行gen函式體內程式碼的唯一方法就是在返回的迭代器物件上呼叫next方法。每一次呼叫next,函式體內程式碼就執行到一個yield表示式處,這個表示式的右值賦值給迭代器
  3. next方法也可以接受引數,使用引數呼叫將會用引數值替換上一條yield表示式的左值,然後執行並返回當前yield表示式的右值
const a = yiled foo();
//    |           |
//    |           |
//   左值         右值
複製程式碼

請反覆理解上述步驟或者參考MDN文件

這些特性如何幫助我們?

到目前為止,你可能會疑惑,generator函式如何表達本文意圖?

我們需要建立一個非同步工作流模型:即我們需要進行下一步時,必須等待特定任務結束。

但是到目前為止,我們討論的東西都是同步的。怎麼辦?

譯者注:上文的yield表示式後面全是同步值

關鍵點是生成器函式可以對promises進行yield

一個generator函式可以對promise進行yield,並且它的迭代器可以被控制停止並等待promise最終resolvereject並對他們決議的值進行下一步處理。這種構造一個可yield promises的迭代器的模式可以滿足我們的需求:

Notice how this generator function resembles our async function!

目前為止我們只進行到一半。我們需要一個執行函式體內容的方法,我們需要一個可以控制generator函式迭代器的函式,它能夠停止並等待每一個yield promise決議的結果。聽上去很複雜,但是實現起來還是很簡單的 :

A function that executes a generator function. (Only for explanation, do not use it !)

現在我們可以像下面一樣去使用runner函式執行我們的生成器函式init

Use `runner` to execute the body of `init`.

就這麼簡單!runner函式和init函式的組合使用達到了原生async函式的效果。

請切記這個runner函式僅僅是為了講解本文意圖而做的演示程式碼,它不適合實際開發場景,如果你需要一個合適的實現,你可以在這裡找找。

總結

我們開始於一個async函式,然後利用generatorspromises去實現相同功能:

【譯】Async-Await≈Generators+Promises

深入實踐

  • 本文伊始,我們看到BabelES2017async函式轉換之後,如何利用ES2016generatorspromises去實現。你可以回顧一下之前轉換之後的_asyncToGenerator函式,比較我們的runner函式就會發現兩者很相似。實際上,_asyncToGenerator函式是我們這裡極其簡單的runner函式萬無一失的版本

  • 如果你還有興趣,你可以進行下一步研究,即把async函式轉換成沒有generatorsES2015版本程式碼。這樣你可能需要去模擬generators本身(參見regenerator project

我希望通過這篇文章撥開async函式的迷霧,他提供了簡單的語法,減少了程式碼噪聲。async函式的提議是這樣描述的:

The introduction of Promises and Generators in ECMAScript presents an opportunity to dramatically improve the language-level model for writing asynchronous code in ECMAScript.

感謝Akos, Alisa以及Kristian為完善這篇文章所提供的反饋。

相關文章