javascript非同步程式設計之generator(生成器函數)與asnyc/await語法糖

2022-11-02 06:01:27

Generator 非同步方案

相比於傳統回撥函數的方式處理非同步呼叫,Promise最大的優勢就是可以鏈式呼叫解決回撥巢狀的問題。但是這樣寫依然會有大量的回撥函數,雖然他們之間沒有巢狀,但是還是沒有達到傳統同步程式碼的可讀性。如果以下面的方式寫非同步程式碼,它是很簡潔,也更容易閱讀的。

// like sync mode

try{
  const value1 = ajax('/api/url1')
  console.log(value1)
  const value2 = ajax('/api/url1')
  console.log(value2)
  const value3 = ajax('/api/url1')
  console.log(value3)
  const value4 = ajax('/api/url1')
  console.log(value4)
  const value5 = ajax('/api/url1')
  console.log(value5)
}catch(err){
  console.log(err)
}
  

ES2015提供了生成器函數(Generator Function)它與普通函數的語法差別在於,在function語句之後和函數名之前,有一個「*」作為生成器函數的標示符。

在我們去呼叫生成器函數的時候他並不會立即去執行這個函數,而是會得到一個生成器物件,直到我們手動呼叫物件的next 方法,函數體才會開始執行,我們可以使用關鍵字yield去向外返回一個值,我們可以在next方法的返回值中去拿到這個值。另外再返回的屬性中還有一個done關鍵字來表示生成器是否執行完了,

yield不會像return一樣去結束函數的執行,只是暫停函數的執行,直到外接下一次呼叫next方法時才會繼續從yield位置往下執行

function * foo () {
  console.log('start')
	yield 'foo'
}

const generator = foo()

const result = generator.next()

呼叫next方法的時候傳入了引數的話,所傳入的引數會作為yield關鍵字的返回值

function * foo () {
  console.log('start')
	// 我可以在這裡接收next傳入的引數
	const res = yield 'foo'
  console.log(res) // 這是我傳入的引數
}

const generator = foo()

const result = generator.next('這是我傳入的引數')
console.log(result) // { value: 'foo', done: false }

如果我們呼叫了生成器函數的throw方法,這個方法會給生成器函數內部丟擲一個異常

function * foo () {
  console.log('start')
  // 我可以在這裡接收next傳入的引數
  try {
    const res = yield 'foo'
    console.log(res) // 這是我傳入的引數
  } catch (err) {
    console.log(err.message) // 丟擲錯誤
  }
}

const generator = foo()

const result = generator.next('這是我傳入的引數')
console.log(result)

generator.throw(new Error('丟擲錯誤'))

利用生成器函數和Promise來實現非同步程式設計的體驗

function ajax(url) {
  return new Promise((resove, reject) => {
    var xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    // 新方法可以直接接受一個j物件
    xhr.responseType = 'json'
    xhr.onload = function () {
      if (this.status === 200) {
        resove(this.response)
      } else {
        reject(new Error(this.statusText))
      }
    }
    xhr.send()
  })
}

function* main() {
  const user1 = yield ajax('/json1.json')
  console.log(user1)

  const user2 = yield ajax('/json2.json')
  console.log(user2)

  const user3 = yield ajax('/json3.json')
  console.log(user3)
}

const g = main()
const result = g.next()

result.value.then(data => {
  const result2 = g.next(data)

  if (result2.done) return
  result2.value.then(data2 => {
    const result3 = g.next(data2)

    if (result3.done) return
    result3.value.then(data3 => {
      g.next(data3)
    })
  })
})

很明顯生成器的執行器可以使用遞迴的方式去呼叫

const g = main()

function handleResult(result) {
  if (result.done) return
  result.value.then(data => {
    handleResult(g.next(data))
  }, err => {
    g.throw(err)
  })
}

handleResult(g.next())

生成器函數的呼叫其實都是差不多的,所以我們可以寫一個比較通用的執行器

function co(generator) {
  const g = generator()

  function handleResult(result) {
    if (result.done) return
    result.value.then(data => {
      handleResult(g.next(data))
    }, err => {
      g.throw(err)
    })
  }

  handleResult(g.next())
}


co(main)

當然這樣的執行器在社群中已經有一個比較完善的庫了co。這種co的方案在2015年之前是特別流行的,後來在出了async/await語法糖之後,這種方案相對來講就沒有那麼普及了。使用generator這種方法最明顯的變化就是非同步呼叫回歸到扁平化了

async/await

有了generator之後js非同步程式設計基本上與同步程式碼有類似的體驗了,但是使用generator這種非同步方案還需要自己手動去寫一個執行器函數,會比較麻煩。在ES2017的版本中新增了一個叫做async的函數,它同樣提供了這種扁平化的程式設計體驗,並且是語言層面的標準的非同步程式設計語法。其實async函數就是生成器函數更方便的語法糖,所以語法上給generator函數是類似的。

async function main() {
  try {
    const user1 = await ajax('/json1.json')
    console.log(user1)

    const user2 = await ajax('/json2.json')
    console.log(user2)

    const user3 = await ajax('/json3.json')
    console.log(user3)
  } catch (error) {
    console.log(error)
  }
}

main()

async 函數返回一個Promise物件,更利於對整體程式碼控制

promise.then(() => {
  console.log('all completed')
}).catch(err => {
  console.log(err)
})

原文地址: https://kspf.xyz/archives/21
更多內容微信公眾號搜尋充飢的泡飯
小程式搜一搜開水泡飯的部落格