async函數

本系列屬於阮一峯老師所著的ECMAScript 6 入門學習筆記javascript


概念

ES2017中引入了async函數,使異步操做變得更加方便。async是Generator函數的語法糖。java

const gen = function* (){
  const f1 = yield readFile('/etc/fstab')
  const f2 = yield readFile('/etc/shells')
}

// 寫成async函數
const gen = async function (){
  const f1 = await readFile('/etc/fstab')
  const f2 = await readFile('/etc/shells')
}

比較可知,async函數將Generator函數的*替換成async,將yield替換成await ,僅此而已es6

async函數對Generator函數的改進有如下四點:shell

(1)內置執行器promise

Generator函數執行須要依靠執行器,而async函數自帶執行器。也就是說async函數的執行與普通函數同樣併發

gen()

(2)更好的語義化異步

asyncawait比起*yield,語義更加清楚。async表示函數有異步操做,await表示緊跟在後面的表達式須要等待結果async

(3)更廣的適用性函數

co模塊約定,yield命令後面只能是Thunk函數或者Promise對象,而async函數的await命令後面能夠是Promise對象和原始類型的值(數值、字符串和布爾值,但這等同於同步操做)學習

(4)返回值是Promise

async函數的返回值是Promise對象,這比Generator函數的返回值是Iterator對象方便多了,咱們能夠用then指定下一步的操做

基本用法

async函數會返回一個Promise對象,能夠使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體後面的語句。

function timeout(ms){
  return new Promise(resolve =>{
    setTimeout(resolve,ms)
  })
}
// 因爲async函數返回的是Promise函數,所以可改寫爲
async function timeout(ms){
  await new Promise(resolve =>{
    setTimeout(resolve,ms)
  })
}

async function asyncPrint(value,ms){
  await timeout(ms)
  console.log(value)
}

asyncPrint('hello world',50) // 在50毫秒以後輸出hello world

// async函數的多種使用形式

// 函數聲明
async function foo(){}

// 函數表達式
const foo = async function(){}

// 對象的方法
let obj = {async foo(){}};
obj.foo().then()

// class的方法
class Storage{
  constructor(){
    this.cachePromise = caches.open('avatars')
  }
  
  async getAvatar(name){
    const cache = await this.cachePromise
    return cache.match(`/avatars/${name}.jpg`)
  }
}

const storage = new Storage()
storage.getAvatar('jake').then()

// 箭頭函數
const foo = async () => {}
返回Promise對象
// async函數內部return語句返回的值,會成爲then方法回調函數的參數
async function f(){
  return 'hello world'
}

f().then(v => console.log(v)) // 'hello world'

// async函數內部拋出錯誤,會致使返回的Promise對象變成reject狀態,拋出的錯誤對象被catch回調函數接收
async function f(){
  throw new Error('出錯了')
}

f().then(v => console.log(v),e => console.log(e)) // Error:出錯了

// async函數返回的Promise對象,必須等內部全部的await命令後面的Promise對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤
await命令
// 正常狀況下,await命令後面是一個Promise對象。若是不是,會被轉成一個當即resolve的Promise對象
async function f(){
  return await 123
}

f().then(v => console.log(v))

// await後面的Promise對象變爲reject狀態,則reject的參數會被catch方法的回調函數接收到
async function f(){
  await Promise.reject('出錯了')
}

f().then(v => console.log(v)).catch(e => console.log(e)) // 出錯了

// 只要有一個await語句後面的Promise變爲reject,那麼整個async函數都會中斷執行
async function f(){
  await Promise.reject('出錯了')
  await Promise.resolve('Hello world') // 不會執行
}

// 若是但願前一個異步操做失敗,也不中斷後面的異步操做,這時能夠把第一個await放在try...catch結構裏面,這樣不管這個異步操做是否成功,第二個await都會執行
async function f(){
  try{
    await Promise.reject('出錯了')
  }catch(e){
  }
  return await Promise.resolve('hello world')
}

f().then(v => console.log(v)) // 'hello world'
  
// 另一種方法是await後面的Promise對象再跟一個catch方法,處理前面可能出現的錯誤
async function f(){
  await Promise.reject('出錯了').catch(e => console.log(e))
  return await Promise.resolve('hello world')
}

f().then(v => console.log(v)) // 出錯了 hello world

// 若是有多個await命令,能夠統一放在try...catch結構中
使用注意點

(1)作好錯誤處理,最好把await命令放在try...catch代碼塊中

(2)多個await命令後面的異步操做,若是不存在繼發關係,最好讓它們同時觸發

let foo = await getFoo()
let bar = await getBar()

// 這兩個獨立的異步操做互不影響,被寫成繼發關係,這樣比較耗時,可讓他們同時觸發,縮短程序的執行時間
// 寫法一
let [foo,bar] = await Promise.all([getFoo(),getBar()])
// 寫法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise

(3)await只能用在async函數中,若是用在普通函數,就會報錯

async函數的實現原理

async函數的實現原理,就是將Generator函數和自動執行器,包裝在一個函數裏

async function fn(args){
  // ...
}

// 等同於

function fn(args){
  return spawn(function* (){
    // ...
  })
}
// 其中spawn函數就是自動執行器
與其餘異步方法的比較

如下例子比較async函數、Promise、Generator函數

// 某個DOM元素,部署了一系列的動畫,前一個動畫結束,纔開始後一個。若是其中有一個動畫出錯,就再也不繼續執行了,返回上一個成功執行的動畫的返回值

// Promise的寫法
function chainAnimationsPromise(elem,animations){
  // 變量ret用來保存上一個動畫的返回值
  let ret = null
  // 新建一個空的Promise
  let p = Promise.resolve()
  // 使用then方法,添加全部動畫
  for(let anim of animations){
    p = p.then(function(val){
      ret = val
      return anim(elem)
    })
  }
  // 返回一個部署了錯誤機制的Promise
  return p.catch(function(e){
    // 忽略錯誤,繼續執行
  }).then(function(){
    return ret
  })
}
// Promise寫法已經比回調函數的寫法大大改進,但操做自己的語義不太容易看出來

// Generator函數的寫法
function chainAnimationsGenerator(elem,animations){
  return spawn(function* (){
    let ret = null
    try{
      for(let anim of animations){
        ret = yield anim(elem)
      }
    }catch(e){
      // 忽略錯誤,繼續執行
    }
    return ret
  })
}
// Generator函數須要一個spawn自動執行器,並且必須保證yield語句後面的表達式必須返回一個Promise

// async函數的寫法
async function chainAnimationsAsync(elem,animations){
  let ret = null
  try{
    for(let anim of animations){
      ret = await anim(elem)
    }
  }catch(e){
    //忽略錯誤,繼續執行
  }
  return ret
}
// async函數的實現最簡潔,最符合語義,代碼量少
實例:按順序完成異步操做
// 一次遠程讀取一組URL,按照讀取的順序輸出結果

// Promise的寫法
function logInOrder(urls){
  // 遠程讀取全部URL
  const textPromises = urls.map(url =>{
    return fetch(url.then(response => response.text()))
  })
  // 按次序輸出
  textPromises.reduce((chain,textPromise) => {
    return chain.then(() => textPromise).then(text => console.log(text))
  },Promise.resolve())
}

// async函數寫法
async function logInOrder(urls){
  for(const url of urls){
    const response = await fetch(url)
    console.log(await response.text())
  }
}

// 這樣簡化了寫法,可是全部操做都是繼發,效率差,咱們須要併發請求
async function logInOrder(urls){
  // 併發讀取遠程URL
  const textPromises = urls.map(async url =>{
    const response = await fetch(url)
    return reponse.text()
  })
  // 按次序輸出
  for(const textPromise of textPromises){
    console.log(await textPromise)
  }
}
// map方法的參數是async函數,但他是併發執行的,由於只有async函數內部是繼發執行,外部不受影響。後面的for...of循環內部使用await,所以實現了按順序輸出
相關文章
相關標籤/搜索