JavaScript異步編程:Generator與Async


Promise開始,JavaScript就在引入新功能,來幫助更簡單的方法來處理異步編程,幫助我們遠離回調地獄。
Promise是下邊要講的Generator/yieldasync/await的基礎,希望你已經提前了解了它。

在大概ES6的時代,推出了Generator/yield兩個關鍵字,使用Generator可以很方便的幫助我們建立一個處理Promise的解釋器。

然后,在ES7左右,我們又得到了async/await這樣的語法,可以讓我們以接近編寫同步代碼的方式來編寫異步代碼(無需使用.then()或者回調函數)。

兩者都能夠幫助我們很方便的進行異步編程,但同樣,這兩者之間也是有不少區別的。

Generator

Generator是一個函數,可以在函數內部通過yield返回一個值(此時,Generator函數的執行會暫定,直到下次觸發.next()
創建一個Generator函數的方法是在function關鍵字后添加*標識。

在調用一個Generator函數后,並不會立即執行其中的代碼,函數會返回一個Generator對象,通過調用對象的next函數,可以獲得yield/return的返回值。
無論是觸發了yield還是returnnext()函數總會返回一個帶有valuedone屬性的對象。
value為返回值,done則是一個Boolean對象,用來標識Generator是否還能繼續提供返回值。
P.S. Generator函數的執行時惰性的,yield后的代碼只在觸發next時才會執行

 1 function * oddGenerator () {
 2   yield 1
 3   yield 3
 4 
 5   return 5
 6 }
 7 
 8 let iterator = oddGenerator()
 9 
10 let first = iterator.next()  // { value: 1, done: false }
11 let second = iterator.next() // { value: 3, done: false }
12 let third = iterator.next()  // { value: 5, done: true  }

 

next的參數傳遞

我們可以在調用next()的時候傳遞一個參數,可以在上次yield前接收到這個參數:

 1 function * outputGenerator () {
 2   let ret1 = yield 1
 3   console.log(`got ret1: ${ret1}`)
 4   let ret2 = yield 2
 5   console.log(`got ret2: ${ret2}`)
 6 }
 7 
 8 let iterator = outputGenerator()
 9 
10 iterator.next(1)
11 iterator.next(2) // got ret1: 2
12 iterator.next(3) // got ret2: 3

 

第一眼看上去可能會有些詭異,為什么第一條log是在第二次調用next時才進行輸出的
這就又要說到上邊的Generator的實現了,上邊說到了,yieldreturn都是用來返回值的語法。
函數在執行時遇到這兩個關鍵字后就會暫停執行,等待下次激活。
然后let ret1 = yield 1,這是一個賦值表達式,也就是說會先執行=右邊的部分,在=右邊執行的過程中遇到了yield關鍵字,函數也就在此處暫停了,在下次觸發next()時才被激活,此時,我們繼續進行上次未完成的賦值語句let ret1 = XXX,並在再次遇到yield時暫停。
這也就解釋了為什么第二次調用next()的參數會被第一次yield賦值的變量接收到

用作迭代器使用

因為Generator對象是一個迭代器,所以我們可以直接用於for of循環:

但是要注意的是,用作迭代器中的使用,則只會作用於yield
return的返回值不計入迭代

 1 function * oddGenerator () {
 2   yield 1
 3   yield 3
 4   yield 5
 5 
 6   return 'won\'t be iterate'
 7 }
 8 
 9 for (let value of oddGenerator()) {
10   console.log(value)
11 }
12 // > 1
13 // > 3
14 // > 5

 

Generator函數內部的Generator

除了yield語法以外,其實還有一個yield*語法,可以粗略的理解為是Generator函數版的[...]
用來展開Generator迭代器的。

 1 function * gen1 () {
 2   yield 1
 3   yield* gen2()
 4   yield 5
 5 }
 6 
 7 function * gen2 () {
 8   yield 2
 9   yield 3
10   yield 4
11   return 'won\'t be iterate'
12 }
13 
14 for (let value of gen1()) {
15   console.log(value)
16 }
17 // > 1
18 // > 2
19 // > 3
20 // > 4
21 // > 5

 

模擬實現Promise執行器

然后我們結合着Promise,來實現一個簡易的執行器。

最受歡迎的類似的庫是: co

 1 function run (gen) {
 2   gen = gen()
 3   return next(gen.next())
 4 
 5   function next ({done, value}) {
 6     return new Promise(resolve => {
 7      if (done) { // finish
 8        resolve(value)
 9      } else { // not yet
10        value.then(data => {
11          next(gen.next(data)).then(resolve)
12        })
13      }
14    })
15   }
16 }
17 
18 function getRandom () {
19   return new Promise(resolve => {
20     setTimeout(_ => resolve(Math.random() * 10 | 0), 1000)
21   })
22 }
23 
24 function * main () {
25   let num1 = yield getRandom()
26   let num2 = yield getRandom()
27 
28   return num1 + num2
29 }
30 
31 run(main).then(data => {
32   console.log(`got data: ${data}`);
33 })

 

一個簡單的解釋器的模擬(僅作舉例說明)

在例子中,我們約定yield后邊的必然是一個Promise函數
我們只看main()函數的代碼,使用Generator確實能夠讓我們讓近似同步的方式來編寫異步代碼
但是,這樣寫就意味着我們必須有一個外部函數負責幫我們執行main()函數這個Generator,並處理其中生成的Promise,然后在then回調中將結果返回到Generator函數,以便可以執行下邊的代碼。

Async

我們使用async/await來重寫上邊的Generator例子:

 1 function getRandom () {
 2   return new Promise(resolve => {
 3     setTimeout(_ => resolve(Math.random() * 10 | 0), 1000)
 4   })
 5 }
 6 
 7 async function main () {
 8   let num1 = await getRandom()
 9   let num2 = await getRandom()
10 
11   return num1 + num2
12 }

 

 console.log(`got data: ${await main()}`) 

這樣看上去,好像我們從Generator/yield換到async/await只需要把*都改為asyncyield都改為await就可以了。 所以很多人都直接拿Generator/yield來解釋async/await的行為,但這會帶來如下幾個問題:

  1. Generator有其他的用途,而不僅僅是用來幫助你處理Promise
  2. 這樣的解釋讓那些不熟悉這兩者的人理解起來更困難(因為你還要去解釋那些類似co的庫)

async/await是處理Promise的一個極其方便的方法,但如果使用不當的話,也會造成一些令人頭疼的問題

Async函數始終返回一個Promise

一個async函數,無論你return 1或者throw new Error()
在調用方來講,接收到的始終是一個Promise對象:

1 async function throwError () {
2   throw new Error()
3 }
4 async function returnNumber () {
5   return 1
6 }
7 
8 console.log(returnNumber() instanceof Promise) // true
9 console.log(throwError() instanceof Promise)   // true

 

也就是說,無論函數是做什么用的,你都要按照Promise的方式來處理它。

Await是按照順序執行的,並不能並行執行

JavaScript是單線程的,這就意味着await一只能一次處理一個,如果你有多個Promise需要處理,則就意味着,你要等到前一個Promise處理完成才能進行下一個的處理,這就意味着,如果我們同時發送大量的請求,這樣處理就會非常慢,one by one

1 const bannerImages = []
2 
3 async function getImageInfo () {
4   return bannerImages.map(async banner => await getImageInfo(banner))
5 }

 

就像這樣的四個定時器,我們需要等待4s才能執行完畢:

 1 function delay () {
 2   return new Promise(resolve => setTimeout(resolve, 1000))
 3 }
 4 
 5 let tasks = [1, 2, 3, 4]
 6 
 7 async function runner (tasks) {
 8   for (let task of tasks) {
 9     await delay()
10   }
11 }
12 
13 console.time('runner')
14 await runner(tasks)
15 console.timeEnd('runner')

 

像這種情況,我們可以進行如下優化:

 1 function delay () {
 2   return new Promise(resolve => setTimeout(resolve, 1000))
 3 }
 4 
 5 let tasks = [1, 2, 3, 4]
 6 
 7 async function runner (tasks) {
 8   tasks = tasks.map(delay)
 9   await Promise.all(tasks)
10 }
11 
12 console.time('runner')
13 await runner(tasks)
14 console.timeEnd('runner')

 

草案中提到過await*,但現在貌似還不是標准,所以還是采用Promise.all包裹一層的方法來實現

我們知道,Promise對象在創建時就會執行函數內部的代碼,也就意味着,在我們使用map創建這個數組時,所有的Promise代碼都會執行,也就是說,所有的請求都會同時發出去,然后我們通過await Promise.all來監聽所有Promise的響應。

結論

Generatorasync function都是返回一個特定類型的對象:

  1. Generator: 一個類似{ value: XXX, done: true }這樣結構的Object
  2. Async: 始終返回一個Promise,使用await或者.then()來獲取返回值

Generator是屬於生成器,一種特殊的迭代器,用來解決異步回調問題感覺有些不務正業了。。 而async則是為了更簡潔的使用Promise而提出的語法,相比Generator + co這種的實現方式,更為專注,生來就是為了處理異步編程。

現在已經是2018年了,async也是用了好久,就讓Generator去做他該做的事情吧。。

參考資料

 示例代碼:code-resource


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM