理解 Javascript 的 async await

PVKElwood 8年前發布 | 16K 次閱讀 HTML JavaScript開發 JavaScript

async / await 并沒有作為 ES2016 的一部分, 但這不意味著 Javascript 不會加入 這一語法特性。就在本文撰寫的此刻,它正處于 Stage 3 的階段, 并處于活躍更新狀態. 這個功能 在 Edge 里已經可用 , 并且 如果在更多瀏覽器中被實現則進入 Statge 4 —— 可以說,下個版本該功能已經在路上了.

我們聽說過這個特性已經有段時間了,但是并沒有真正深入的探索它到底是怎樣工作的。本文會幫助你理解這方面的內容,但此之前需要你對 promise 和 generator 已經有所了解。

用 Promise

假設我們有如下代碼,我們將一個 HTTP 請求封裝在一個 Promise 里,若請求成功則將 body 成功返回,否則將 err reject 出來。這個請求每次會拉取本博客里的隨機一篇文章的 HTML 內容。

var request = require('request');

function getRandomPonyFooArticle () {
  return new Promise((resolve, reject) => {
    request('https://ponyfoo.com/articles/random', (err, res, body) => {
      if (err) {
        reject(err); return;
      }
      resolve(body);
    });
  });
}

典型的使用上面 promise 的方法如下代碼所示。我們構造了一個 promise 鏈,將 HTML 頁面的一部分子集 DOM 結構轉換為對應的 markdown 文檔,并最終以適用于終端的方式用 console.log 打印出來。記住最好給你的 promise 都加上 .catch 錯誤處理。

var hget = require('hget');
var marked = require('marked');
var Term = require('marked-terminal');

printRandomArticle();

function printRandomArticle () {
  getRandomPonyFooArticle()
    .then(html => hget(html, {
      markdown: true,
      root: 'main',
      ignore: '.at-subscribe,.mm-comments,.de-sidebar'
    }))
    .then(md => marked(md, {
      renderer: new Term()
    }))
    .then(txt => console.log(txt))
    .catch(reason => console.error(reason));
}

以上代碼運行起來的效果如下截圖所示。

運行截圖

從代碼可讀性的角度來看,這段代碼比使用回調更好更更有序。

使用 Generator

我們 之前已經了解怎么使用 generator 通過一種偽"同步"的方式構建可用的 html . 雖然當時的代碼某種程度上可以說是同步的代碼,但是需要包裹很多代碼結構,而且 generator 也許并不是最直接的能夠達成我們目的方式,所以我們可能還是要依靠 Promise.

function getRandomPonyFooArticle (gen) {
  var g = gen();
  request('https://ponyfoo.com/articles/random', (err, res, body) => {
    if (err) {
      g.throw(err); return;
    }
    g.next(body);
  });
}

getRandomPonyFooArticle(function* printRandomArticle () {
  var html = yield;
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
});

記住你需要在 yield 外面加上 try / catch 來捕獲之前 promise 的錯誤處理。

像這樣使用 generator 不易于擴展這一點是不言自明的。況且由于這種不是很自觀的語法,你的迭代器代碼需要和你使用的 generator 高度耦合。這意味著想要向你的 generator 中加入新的 await 表達式需要頻繁修改代碼。比較好的替代方法是使用即將到來的 async 函數

使用 async / await

使用 async 函數 時我們可以實現基于 Promise 的類似 generator 那樣寫同步代碼的方式。另一個好處是你不需要修改 getRandomPonyFooArticle ,只要它返回的是一個 promise,它就可以使用 await 獲取.

注意 await 只能用于標注了 async 關鍵字的函數內部 。它的工作方式類似 generator,在 promise 確定狀態之前它的執行流程會掛起。如果 await 的不是 promise,會自動轉化為一個 promise.

read();

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  console.log(txt);
}

和 generator 一樣,記住你應該把 await 部分放到 try / catch 里,用這種方式對 await 的那個 promise 進行錯誤捕獲和處理。

另外,一個 Async 函數 總是返回一個 Promise . 未捕獲異常會被這個 promise reject,否則 promise 會 resolve 這個 async 函數的返回值。因此我們可以調用一個 aysnc 函數并將其和 promise 的鏈式調用方法相結合,接下來的實例看看怎么組合使用這兩者.

async function asyncFun () {
  var value = await Promise
    .resolve(1)
    .then(x => x * 3)
    .then(x => x + 5)
    .then(x => x / 2);
  return value;
}
asyncFun().then(x => console.log(`x: ${x}`));
// <- 'x: 4'

回到前面一個例子,這意味著我們可以在 async read 函數里 return txt , 這樣用戶可以接著使用 promise 或者另一個 async 函數。這樣你的 read 函數只需要關注怎么從 Pony Foo 獲取一篇隨機文章并轉換為終端可讀的 markdown 形式。

async function read () {
  var html = await getRandomPonyFooArticle();
  var md = hget(html, {
    markdown: true,
    root: 'main',
    ignore: '.at-subscribe,.mm-comments,.de-sidebar'
  });
  var txt = marked(md, {
    renderer: new Term()
  });
  return txt;
}

然后,你可以進一步在另一個 Async 函數 里 await read() .

async function write () {
  var txt = await read();
  console.log(txt);
}

或者直接使用 promise 以進行更多后續處理。

read().then(txt => console.log(txt));`

重要抉擇

在異步代碼流程中,并行執行兩個甚至多個任務的情形十分常見。 Async 函數 使編寫異步代碼變得簡單,同時它們也可以用在 串行 的代碼中,亦即,那些 同一時間只執行一個操作 的代碼。內部包含多個 await 表達式的函數,在每個 await 表達式處都會掛起,直到 Promise 的狀態確定并繼續執行到下一個 await 表達式—— 這和我們觀察到的 generator 和 yield 的行為略有不同

繞開這一點的辦法是使用 Promise.all 創建一個單獨的 promise ,然后 await 這個 promise. 當然,最大的問題是培養使用 Promise.all 的習慣而不是讓所有事情都序列執行,后者會拖累你代碼的性能表現。

接下來的例子展示如何 await 三個不同的 promise,同時讓它們完全可以并發執行。 await 會掛起你的 async 函數并且 await Promise.all 表達式最終會 resolve 為一個 results 數組,我們可以通過解構拿到數組里單獨的每一個結果。

async function concurrent () {
  var [r1, r2, r3] = await Promise.all([p1, p2, p3]);
}

在某段歷史時期,上面的代碼可以用 await* 來實現,你不需要將你的 promise 用 Promise.all 包起來, Babel 5 支持這個特性。但是因為 某些原因 標準已經不再支持這種用法 了(Babel 6 也不支持)。

async function concurrent () {
  var [r1, r2, r3] = await* [p1, p2, p3];
}

你仍然可以使用 all = Promise.all.bind(Promise) 得到一個簡潔版本的 Promise.all ,你也可以對 Promise.race 做同樣的事情,即便它沒有類似 await* 的等同寫法。

const all = Promise.all.bind(Promise);
async function concurrent () {
  var [r1, r2, r3] = await all([p1, p2, p3]);
}

異常處理

注意在一個 async 函數里 異常會被悄無聲息地吞沒 ,就和在一個 Promise 里發生的一樣。除非我們為 await 表達式顯式加上 try / catch 塊,否則未捕獲異常——不管是在 async 函數體內部還是在 await 的掛起執行部分拋出——都會被 async 函數返回的 promise 直接 reject.

這里自然可以視為一個優點:你可以延續 try / catch 的傳統,這在 callback 里是沒法做的——而且在 promise 里不知怎么的又是可以的。在這一點上, Async 函數和 generator 是雷同的,你都可以使用 try / catch ,因為它們都將異步流程通過執行函數掛起的方式變為同步代碼。

更進一步,你還可以在 async 函數的外部捕獲異常,只需要在返回的 promise 后面加上 .catch 語句即可。這種使用 .catch 語句實現 try / catch 的異常捕獲機制是一種彈性的方式,但是可能讓人感覺迷惑并最終導致異常沒有被處理。

read()
  .then(txt => console.log(txt))
  .catch(reason => console.error(reason));

我們有必要小心對待并學習通過不同的方法來對異常進行處理,記錄以及避免它們。

今天使用 async / await

在你的代碼里使用 Async 函數的一種方法是 Babel. 這涉及到一系列的模塊,但是 你總是可以找到某一個模塊 ,如果你喜歡的話它會幫助你解決所有這些事情。我一般使用 npm-run 讓所有模塊都只需要裝在本地。

npm i -g npm-run
npm i -D \
  browserify \
  babelify \
  babel-preset-es2015 \
  babel-preset-stage-3 \
  babel-runtime \
  babel-plugin-transform-runtime

echo '{
  "presets": ["es2015", "stage-3"],
  "plugins": ["transform-runtime"]
}' > .babelrc

下面這條命令會把 example.js 通過 browserify 進行編譯,并使用 babelify 使之支持 Async 函數。接著將代碼 pipe 給 node 執行或者存儲到磁盤文件。

The following command will compile example.js through browserify while using babelify to enable support for Async Functions . You can then pipe the script to node or save it to disk.

npm-run browserify -t babelify example.js | node`

參考閱讀

Async 函數 標準草案 并不長,如果你想了解更多關于這一新特性,閱讀它會是有趣的體驗。

我粘貼了一段代碼在下面,以幫助你理解 async 函數的內部是怎樣工作的。即便我們不能使用新關鍵詞的 polyfill,但是理解 async / await 背后的原理對你仍然是有幫助的。

換句話說,學習 Async 函數對使用 generator 和 promise 絕對是有幫助的。

以下代碼展示了怎樣把一個 async function 聲明轉換成普通的 function ,它返回將一個 generator 作為參數傳遞給 spawn 的調用結果,其內部的 await 在句法上和 yield 完全等同。

async function example (a, b, c) {
  example function body
}

function example (a, b, c) {
  return spawn(function* () {
    example function body
  }, this);
}

在 spawn 里返回了一個 promise, 它封裝了將 generator 函數—— 源于用戶代碼 ——進行逐步迭代的一段代碼,串行的傳遞值給你的 "generator" 代碼 ( async 函數的函數體)。我們可以發現 Async 函數 其實就是基于 generator 和 promise 的語法糖,這一點對于我們更好的理解這些概念的工作原理以方便我們更好的混合,匹配以及組合這些不同的異步代碼流程的使用是非常重要的。

function spawn (genF, self) {
  return new Promise(function (resolve, reject) {
    var gen = genF.call(self);
    step(() => gen.next(undefined));
    function step (nextF) {
      var next;
      try {
        next = nextF();
      } catch(e) {
        // finished with failure, reject the promise
        reject(e);
        return;
      }
      if (next.done) {
        // finished with success, resolve the promise
        resolve(next.value);
        return;
      }
      // not finished, chain off the yielded promise and `step` again
      Promise.resolve(next.value).then(
        v => step(() => gen.next(v)),
        e => step(() => gen.throw(e))
      );
    }
  });
}

這些代碼能幫助你更好的理解 async / await 怎么對 generator 序列進行迭代求值,并封裝到一個 promise 里的。每一步的 promise 會串接成一個 promise 鏈,直到序列結束或者某個 promise 被 reject,使得整個 generator 函數返回的 promise 的狀態被確定。

 

來自:http://www.zcfy.cc/article/understanding-javascript-s-async-await-1586.html

 

 本文由用戶 PVKElwood 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!