天天看點

ES6 系列之我們來聊聊 Async

async

ES2017 标準引入了 async 函數,使得異步操作變得更加友善。

在異步處理上,async 函數就是 Generator 函數的文法糖。

舉個例子:

// 使用 generator
var fetch = require('node-fetch');
var co = require('co');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var json1 = yield r1.json();
    console.log(json1.bio);
}

co(gen);           

當你使用 async 時:

// 使用 async
var fetch = require('node-fetch');

var fetchData = async function () {
    var r1 = await fetch('https://api.github.com/users/github');
    var json1 = await r1.json();
    console.log(json1.bio);
};

fetchData();           

其實 async 函數的實作原理,就是将 Generator 函數和自動執行器,包裝在一個函數裡。

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

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}           

spawn 函數指的是自動執行器,就比如說 co。

再加上 async 函數傳回一個 Promise 對象,你也可以了解為 async 函數是基于 Promise 和 Generator 的一層封裝。

async 與 Promise

嚴謹的說,async 是一種文法,Promise 是一個内置對象,兩者并不具備可比性,更何況 async 函數也傳回一個 Promise 對象……

這裡主要是展示一些場景,使用 async 會比使用 Promise 更優雅的處理異步流程。

1. 代碼更加簡潔

/**
 * 示例一
 */
function fetch() {
  return (
    fetchData()
    .then(() => {
      return "done"
    });
  )
}

async function fetch() {
  await fetchData()
  return "done"
};           
/**
 * 示例二
 */
function fetch() {
  return fetchData()
  .then(data => {
    if (data.moreData) {
        return fetchAnotherData(data)
        .then(moreData => {
          return moreData
        })
    } else {
      return data
    }
  });
}

async function fetch() {
  const data = await fetchData()
  if (data.moreData) {
    const moreData = await fetchAnotherData(data);
    return moreData
  } else {
    return data
  }
};           
/**
 * 示例三
 */
function fetch() {
  return (
    fetchData()
    .then(value1 => {
      return fetchMoreData(value1)
    })
    .then(value2 => {
      return fetchMoreData2(value2)
    })
  )
}

async function fetch() {
  const value1 = await fetchData()
  const value2 = await fetchMoreData(value1)
  return fetchMoreData2(value2)
};           

2. 錯誤處理

function fetch() {
  try {
    fetchData()
      .then(result => {
        const data = JSON.parse(result)
      })
      .catch((err) => {
        console.log(err)
      })
  } catch (err) {
    console.log(err)
  }
}           

在這段代碼中,try/catch 能捕獲 fetchData() 中的一些 Promise 構造錯誤,但是不能捕獲 JSON.parse 抛出的異常,如果要處理 JSON.parse 抛出的異常,需要添加 catch 函數重複一遍異常處理的邏輯。

在實際項目中,錯誤處理邏輯可能會很複雜,這會導緻備援的代碼。

async function fetch() {
  try {
    const data = JSON.parse(await fetchData())
  } catch (err) {
    console.log(err)
  }
};           

async/await 的出現使得 try/catch 就可以捕獲同步和異步的錯誤。

3. 調試

const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 1))
const fetchMoreData2 = (value) => new Promise((resolve) => setTimeout(resolve, 1000, value + 2))

function fetch() {
  return (
    fetchData()
    .then((value1) => {
      console.log(value1)
      return fetchMoreData(value1)
    })
    .then(value2 => {
      return fetchMoreData2(value2)
    })
  )
}

const res = fetch();
console.log(res);           
ES6 系列之我們來聊聊 Async

因為 then 中的代碼是異步執行,是以當你打斷點的時候,代碼不會順序執行,尤其當你使用 step over 的時候,then 函數會直接進入下一個 then 函數。

const fetchData = () => new Promise((resolve) => setTimeout(resolve, 1000, 1))
const fetchMoreData = () => new Promise((resolve) => setTimeout(resolve, 1000, 2))
const fetchMoreData2 = () => new Promise((resolve) => setTimeout(resolve, 1000, 3))

async function fetch() {
  const value1 = await fetchData()
  const value2 = await fetchMoreData(value1)
  return fetchMoreData2(value2)
};

const res = fetch();
console.log(res);           
ES6 系列之我們來聊聊 Async

而使用 async 的時候,則可以像調試同步代碼一樣調試。

async 地獄

async 地獄主要是指開發者貪圖文法上的簡潔而讓原本可以并行執行的内容變成了順序執行,進而影響了性能,但用地獄形容有點誇張了點……

例子一

(async () => {
  const getList = await getList();
  const getAnotherList = await getAnotherList();
})();           

getList() 和 getAnotherList() 其實并沒有依賴關系,但是現在的這種寫法,雖然簡潔,卻導緻了 getAnotherList() 隻能在 getList() 傳回後才會執行,進而導緻了多一倍的請求時間。

為了解決這個問題,我們可以改成這樣:

(async () => {
  const listPromise = getList();
  const anotherListPromise = getAnotherList();
  await listPromise;
  await anotherListPromise;
})();           

也可以使用 Promise.all():

(async () => {
  Promise.all([getList(), getAnotherList()]).then(...);
})();           

例子二

當然上面這個例子比較簡單,我們再來擴充一下:

(async () => {
  const listPromise = await getList();
  const anotherListPromise = await getAnotherList();

  // do something

  await submit(listData);
  await submit(anotherListData);

})();           

因為 await 的特性,整個例子有明顯的先後順序,然而 getList() 和 getAnotherList() 其實并無依賴,submit(listData) 和 submit(anotherListData) 也沒有依賴關系,那麼對于這種例子,我們該怎麼改寫呢?

基本分為三個步驟:

1. 找出依賴關系

在這裡,submit(listData) 需要在 getList() 之後,submit(anotherListData) 需要在 anotherListPromise() 之後。

2. 将互相依賴的語句包裹在 async 函數中

async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}           

3.并發執行 async 函數

async function handleList() {
  const listPromise = await getList();
  // ...
  await submit(listData);
}

async function handleAnotherList() {
  const anotherListPromise = await getAnotherList()
  // ...
  await submit(anotherListData)
}

// 方法一
(async () => {
  const handleListPromise = handleList()
  const handleAnotherListPromise = handleAnotherList()
  await handleListPromise
  await handleAnotherListPromise
})()

// 方法二
(async () => {
  Promise.all([handleList(), handleAnotherList()]).then()
})()           

繼發與并發

問題:給定一個 URL 數組,如何實作接口的繼發和并發?

async 繼發實作:

// 繼發一
async function loadData() {
  var res1 = await fetch(url1);
  var res2 = await fetch(url2);
  var res3 = await fetch(url3);
  return "whew all done";
}           
// 繼發二
async function loadData(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}           

async 并發實作:

// 并發一
async function loadData() {
  var res = await Promise.all([fetch(url1), fetch(url2), fetch(url3)]);
  return "whew all done";
}           
// 并發二
async function loadData(urls) {
  // 并發讀取 url
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序輸出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}           

async 錯誤捕獲

盡管我們可以使用 try catch 捕獲錯誤,但是當我們需要捕獲多個錯誤并做不同的處理時,很快 try catch 就會導緻代碼雜亂,就比如:

async function asyncTask(cb) {
    try {
       const user = await UserModel.findById(1);
       if(!user) return cb('No user found');
    } catch(e) {
        return cb('Unexpected error occurred');
    }

    try {
       const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
    } catch(e) {
        return cb('Error occurred while saving task');
    }

    if(user.notificationsEnabled) {
        try {
            await NotificationService.sendNotification(user.id, 'Task Created');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    if(savedTask.assignedUser.id !== user.id) {
        try {
            await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
        } catch(e) {
            return cb('Error while sending notification');
        }
    }

    cb(null, savedTask);
}           

為了簡化這種錯誤的捕獲,我們可以給 await 後的 promise 對象添加 catch 函數,為此我們需要寫一個 helper:

// to.js
export default function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]);
}           

整個錯誤捕獲的代碼可以簡化為:

import to from './to.js';

async function asyncTask() {
     let err, user, savedTask;

     [err, user] = await to(UserModel.findById(1));
     if(!user) throw new CustomerError('No user found');

     [err, savedTask] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) throw new CustomError('Error occurred while saving task');

    if(user.notificationsEnabled) {
       const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if (err) console.error('Just log the error and continue flow');
    }
}           

async 的一些讨論

async 會取代 Generator 嗎?

Generator 本來是用作生成器,使用 Generator 處理異步請求隻是一個比較 hack 的用法,在異步方面,async 可以取代 Generator,但是 async 和 Generator 兩個文法本身是用來解決不同的問題的。

async 會取代 Promise 嗎?

  1. async 函數傳回一個 Promise 對象
  2. 面對複雜的異步流程,Promise 提供的 all 和 race 會更加好用
  3. Promise 本身是一個對象,是以可以在代碼中任意傳遞
  4. async 的支援率還很低,即使有 Babel,編譯後也要增加 1000 行左右。

參考

  1. [[譯] 6 個 Async/Await 優于 Promise 的方面]( https://zhuanlan.zhihu.com/p/26260061)
  2. [[譯] 如何逃離 async/await 地獄]( https://juejin.im/post/5aefbb48f265da0b9b073c40)
  3. 精讀《async/await 是把雙刃劍》
  4. ECMAScript 6 入門
  5. How to write async await without try-catch blocks in Javascript

ES6 系列

ES6 系列目錄位址:

https://github.com/mqyqingfeng/Blog

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的了解,重點講解塊級作用域、标簽模闆、箭頭函數、Symbol、Set、Map 以及 Promise 的模拟實作、子產品加載方案、異步處理等内容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

繼續閱讀