淺談async/await

背景

ES7提出的async/await是JavaScript爲了解決異步問題而提出的一種解決方案,沒有更多的回調,許多人將其稱爲異步的終極解決方案。async函數是Generator函數的語法糖。使用關鍵字async表示,在函數內部使用await表示異步。JavaScript的發展也經歷了回調、Promise、async/await三個階段,該文章寫了我自己對於async/await的理解。不對的地方還請大家幫忙指出,共同進步。

概念

async是“異步”的簡寫,async申明一個function是異步的,而await是等待一個異步方法執行完成,await 只能出現在async函數中。
async函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

  • async的作用
    async函數負責返回一個Promise 對象;如果在async函數中return一個變量,async 會把這個變量通過Promise.resolve() 封裝成Promise對象;如果 async函數沒有返回值,它會返回Promise.resolve(undefined)。
  • await在等待什麼

一般我們都用await等待一個async函數完成,await 等待的是一個表達式,這個表達式的計算結果是Promise 對象或者其它值,所以,await後面實際可以接收普通函數調用或者直接量。

如果await等到的不是一個promise對象,那跟着表達式的運算結果就是它等到的東西;如果是一個promise對象,await會阻塞後面的代碼,等promise對象resolve,得到resolve的值作爲await表達式的運算結果
雖然await阻塞了,但await在async中,async不會阻塞,它內部所有的阻塞都被封裝在一個promise對象中異步執行。

優點

不同於Generator的是,async函數的改進在下面四點:

  • 內置執行器。Generator 函數的執行必須依靠執行器,而async函數自帶執行器,調用方式跟普通函數的調用一樣。
  • 更好的語義。async和await 相較於*和yield更加語義化。
  • 更廣的適用性。co模塊約定,yield命令後面只能是Thunk函數或 Promise對象。而async 函數的await命令後面則可以是Promise或原始類型的值(Number,string,boolean,但這時等同於同步操作)。
  • 返回值是Promise, async函數返回值是Promise對象,比Generator函數返回的Iterator對象方便,可以直接使用then()方法進行調用。

特點

  • 建立在promise之上,所以不能把它和回調函數搭配使用,但它會聲明一個異步函數,並隱式返回一個Promise。因此可以直接return變量,無需使用Promise.resolve進行轉換。
  • 和promise一樣,是非阻塞的,但不用寫then及其回調函數,減少代碼行數,避免了代碼嵌套,而且,所有異步調用可以寫在同一個代碼塊中,無需定義多餘中間變量。
  • 最大價值在於可使異步代碼在形式上更接近同步代碼。
  • 總是與await一起使用的,並且,await只能在async函數體內
  • await是個運算符,用於組成表達式,會阻塞後面代碼。若等到的是Promise對象,則得到其resolve值, 否則,得到一個表達式的運算結果。

async/await使用規則

我們在處理異步時,比起回調函數,Promise的then方法會顯得比較簡潔和清晰,但是在處理多個彼此之間相互依賴的請求時,會顯的有些累贅。這個時候,用async和await會更加優雅。

  • 規則一:凡是在前面添加async的函數在執行後都會自動返回一個Promise對象。
async function fun() {}
let result = fun()
console.log(result)  //即使代碼fun函數什麼都沒返回,依然會打印出Promise對象
  • 規則二:await必須在async函數裏使用,不能單獨使用
async fun() {
   let result = await Promise.resolve('success')
   console.log(result)
}
fun()
  • 規則三:await後面需要跟Promise對象,不然就沒有意義,而且await後面的Promise對象不必寫then,因爲await的作用之一就是獲取後面Promise對象成功狀態傳遞出來的參數。
function fun() {
   return new Promise((resolve, reject) => {
       setTimeout(() => {resolve('hello javascript')})
   })
}
async fn() {
   let result = await fun() //fun會返回一個Promise對象
   console.log(result)    //打印出Promise成功後傳遞過來的'hello javascript'
}

fn()

也可以這麼寫,但是意義不大:

let fun = async function() {
  let result = await 123
  console.log(result)
}
fun()

語法

async函數返回一個Promise 對象
async函數內部return返回的值,是then方法回調函數的參數。

async function  fun() {
    return 'hello node'
};
fun().then( (v) => console.log(v)) // hello node

如果async函數內部拋出異常,則會導致返回的Promise對象狀態變爲 reject狀態。拋出的錯誤會被catch方法回調函數接收到。

async function fun(){
    throw new Error('error');
}
fun().then(v => console.log(v))
.catch( e => console.log(e));

async函數返回的Promise對象,必須等到內部所有的await命令的Promise對象執行完,纔會發生狀態改變
即只有當async函數內部的異步操作都執行完,纔會執行then方法的回調

如下代碼所示:

const time = timeout => new Promise(resolve=> setTimeout(resolve, timeout));
async function f(){
    await time(1000);
    await time(4000);
    await time(7000);
    return 'finish';
}

f().then(v => console.log(v)); // 等待12s後才輸出 'finish'

正常情況下,await 命令後面跟着的是 Promise ,如果不是的話,也會被轉換成一個立即resolve的Promise
如下代碼所示:

async function  fun() {
    return await 1
};
fun().then( (v) => console.log(v)) // 1

如果返回的是reject的狀態,會被catch方法捕獲。

async函數的錯誤處理

其實async函數語法不難,難就在錯誤處理上。有關錯誤處理,如上面規則三所說,await可直接獲取到後面Promise成功狀態傳遞的參數,但卻捕捉不到失敗狀態。我們通過給包裹await的async函數添加then/catch方法來解決,因爲根據規則一,async函數本身就會返回一個Promise對象。

我們先來看一個demo

let b;
async function fun() {
    await Promise.reject('error');
    b = await 1; // 這段await並未執行
}
fun().then(v => console.log(b));
注意:當async函數中只要一個await出現reject狀態,則後面的await都不會被執行。解決辦法:可以添加try/catch。

正確的寫法如下:

// 正確的寫法
let b;
async function fun() {
    try {
        await Promise.reject('error')
    } catch (error) {
        console.log(error);
    }
    b = await 1;
    return b;
}
fun().then(v => console.log(b)); // 1

如果有多個await則可將其都放在try/catch中。
我們來看一個包含錯誤處理的完整demo:

let promise = new Promise((resolve, reject) => {
  setTimeout(() => {
      let random = Math.random()
      if (random >= 0.5) {
          resolve('成功')
      } else {
          reject('失敗')
      }   
  }, 1000)
})
async function fn() {
  let result = await promise
  //result是promise成功狀態的值,如果失敗了,代碼就直接跳到下面的catch了
  return result 
}
fn().then(response => {
  console.log(response) 
}).catch(error => {
  console.log(error)
})
// 最終打印出成功
注意:上述代碼需注意兩個地方,一是async函數需要主動return,如果Promise的狀態是成功的,那麼return的這個值就會被下面的then方法捕捉到;二是如果async函數有任何錯誤,都被catch捕捉到!

同步與異步

在async函數中使用await,await這裏的代碼就會變成同步,意思就是隻有等await後面的Promise執行完成得到結果纔會繼續下去,await就是等待,雖然這樣避免了異步,但是它會阻塞代碼,所以我們在使用的時候需要考慮周全。
我們來看個demo:

function fun1(name) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(`${name}成功執行`)
      }, 1000)
  })
}
async function testfun2() {
  let p1 = await fun1('張三')
  let p2 = await fun1('李四')
  let p3 = await fun1('王麻子')
  return [p1, p2, p3]
}
testfun2().then(result => {
  console.log(result)
}).catch(result => {
  console.log(result)
})

這樣寫雖然是ok的,但是await會阻塞代碼,每個await都必須等後面的fun1()執行完成纔會執行下一行代碼,所以testfun2函數執行需要3秒。如果不是遇到特定的場景,最好不要這樣用。

循環中使用async/await

在循環中使用await,需要牢記一條:必須在async函數中使用。
在for…of中使用await,我們來看個demo:

let fun3 = (time) => {
  return new Promise((resolve) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
let times = [1000, 2500, 3000]
async function testfun3() {
  let result = []
  for (let item of times) {
      let temp = await fun3(item)
      result.push(temp)
  }
  return result
}
testfun3().then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
})
// 最後輸出[ 1000, 2500, 3000 ]

async異步回調併發

1請求、2請求同時發,規定請求到達的順序
假如我們有一種這樣的業務需求,併發兩個請求,但是要規定收到請求的順序應該怎麼做的?這裏借鑑阮一峯大神的代碼:
我們看下demo:

  // 1請求
  function getData1 () {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        console.log('1執行了')
        resolve('請求到模擬數據1')
      }, 2000)
    })
  }
  // 2請求
  function getData2 () {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        console.log('2執行了')
        resolve('請求到模擬數據2!)
      }, 1500)
    })
  }
  async function asyncDemo2 () {
    const arr = [getData1, getData2]
    const textPromises = arr.map(async function (doc) {
      const response = await doc()
      return response
    })
    // 按次序輸出
    for (const textPromise of textPromises) {
      console.log(await textPromise);
    }
  }
  // 2執行了 (因爲2是1500ms後執行) 所以2先執行
  // 1執行了
  // 請求到模擬數據1  (for .. of )規定了輸出的順序
  // 請求到模擬數據2

適用async/await的業務場景

在前端開發中,我們偶爾會遇到這樣的場景:我們需要發送多個請求,而且後面請求的發送總是需要依賴上一個請求返回的數據。對於這個問題,我們既可以用Promise的鏈式調用來解決,也可用async/await來解決,然而後者會更簡潔些。
我們來看個demo:
使用promise鏈式調用處理下面代碼:

function fun2(time) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
fun2(500).then(result => {
  return fun2(result + 1000)
}).then(result => {
  return fun2(result + 1000)
}).then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
}) 
// 最終結果是2500

使用async/await來處理:

function fun2(time) {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
          resolve(time)
      }, time)
  })
}
async function getResult() {
  let p1 = await fun2(500)
  let p2 = await fun2(p1 + 1000)
  let p3 = await fun2(p2 + 1000)
  return p3
}
getResult().then(result => {
  console.log(result)
}).catch(error => {
  console.log(error)
})

從上述代碼中我們可以看出,相對於使用then不停地進行鏈式調用, 使用async/await顯的更加簡潔清晰明瞭易讀一些。

async相對於Promise的優勢

  • 能更好地處理then鏈
  • 中間值
  • 調試,相比於 Promise 更易於調試

我們總結一下async/await的優點:

  • 解決了回調地獄的問題
  • 支持併發執行
  • 可以添加返回值 return 變量
  • 可以在代碼中添加try/catch捕獲錯誤
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章