js異步請求

目前async / await特性並無被添加到ES2016標準中,但不表明這些特性未來不會被加入到Javascript中。在我寫這篇文章時,它已經到達第三版草案,而且正迅速的發展中。這些特性已經被IE Edge支持了,並且它將會到達第四版,屆時該特性將會登錄其餘瀏覽器 -- 爲加入該語言的下一版本而鋪路(也能夠看看:TC39進程)。javascript

咱們據說特性已經有一段時間了,如今讓咱們深刻它,並瞭解它是如何工做的。爲了可以瞭解這篇文章的內容,你須要對promise和生成器對象有深厚的理解。這些資源或許能夠幫到你。php

 

使用Promise

讓咱們假設咱們有像下面這樣的代碼。在這裏我將一個HTTP請求包裝在一個Promise對象中。這個Promise在成功時會返回body對象,被拒絕時會將緣由err返回。它每次都會在本博客(原做者博客)中爲一篇隨機文章拉取html內容。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處理器。java

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)); }

當代碼運行後,這段代碼將產生像如下截圖所示的輸出。node

代碼輸出

上面那段代碼就是「比用回調函數更好」的寫法,它能讓你感受像在按順序的閱讀代碼。react

 

使用生成器(generator)

過去,經過探索,咱們發現生成器能夠用一種「同步」合成的方法來得到html。即便如今的代碼有一些同步寫法,其中仍是涉及至關多的包裝,並且生成器可能不是最直截了當的達到咱們指望結果的方法,最終可能不管如何咱們都會堅持改成使用promise。git

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); });

「請記住,在使用promise時,你應該將yield調用包裝在try/catch塊中來保留咱們添加的錯誤處理器」es6

不說你也知道,像這樣使用生成器並不容易擴展。除了涉及直觀的語法的混入,你的迭代代碼會高度耦合到生成器函數中,這將會下降擴展性。這表示你在添加新的await表達式到生成器中時須要常常修改它。一個更好的替代方案是使用即將到來的Async函數github

 

使用async/await

當Async函數終於落地時,咱們將能夠採起基於Promise的實現方法並使用它的優勢,即像寫同步生成器同樣寫異步。這種作法的另外一個好處是你徹底不須要再去修改getRandomPonyFooArticle方法,在它返回一個承諾前,它會一直等待。shell

要注意的是,await只能在函數中用async關鍵字標記後才能使用 它的工做方式和生成器很類似,直到promise完成以前,會在你的上下文中暫停處理。若是等待表達式不是一個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); }

「再次, -- 跟生成器同樣 -- 記住,你最好把`await`包裝到`try/catch`中,這樣你就能夠在異步函數中對返回後的promise進行錯誤捕獲和處理。」

此外,一個Async函數老是會返回一個Promise對象。 這個promise在出現沒法捕獲的異常時會被拒絕,不然它會處理async函數的返回值。這就容許咱們調用一個async函數並混入常規的基於promise的擴展。如下例子展現了兩個方法的結合(看看Babel的交互式解釋器)。

 

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'

回到前一個例子中,那表示咱們能夠從異步讀取函數中返回文本,而且容許調用者使用promise或另外一個Async函數進行擴展。 那樣,你的讀取函數將只需關注從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表達式的函數在promise對象執行完以前,在恢復執行和移動到下一個await表達式以前,會在每一個await表達式處暫停一次, -- 就跟咱們在生成器和yield關鍵字處觀察到的狀況同樣。

你可使用Promise.all來解決建立單個promise對象並進行等待的功能。 固然,最大的問題是從習慣於讓全部東西都串行運行改爲使用Promise.all, 不然這將給你的代碼帶來性能瓶頸。

下面的例子展現了你如何同時完成對三個不一樣的promise對象進行等待操做。特定的await操做符會暫停你的Async函數,和等待 Promise.all表達式一塊兒,最終會被解析到一個結果數組中,咱們可使用析構函數逐個拉取該數組中的單個結果。

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

在某些狀況下, 能夠用 await *來改動上述代碼片斷,讓你沒必要用Promise.all來包裝你的promise對象。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])`; }

 

錯誤處理

要注意的是,在異步函數中,錯誤會被「默默的」吞噬 -- 就像在普通的Promise對象中同樣。 除非咱們圍繞await表達式添加try/catch塊 -- 而無論在暫停時,它們會在你的異步函數體中發生仍是在它暫停時發生 -- promise對象會被拒絕並經過Async函數返回錯誤。

天然,這能夠看做是一個能力: 你能夠利用try/catch代碼塊,有些東西你沒法用回調函數實現-- 但能夠用Promise對象實現。 在這種狀況下,Async函數就相似生成器,得益於函數執行暫停特性,你能夠利用try/catch將異步流代碼寫成同步代碼的樣子。

此外, 你能夠在Async函數外捕獲這些異常, 只須要簡單的對它們返回的promise對象添加.catch()方法調用。在promise對象中嘗試用.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

在使用babelifyAsync函數提供支持時,如下命令會將example.js經過browserify進行編譯。而後你就能夠用管道將腳本傳輸給node執行,或將腳本保存到硬盤中。

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

 

深刻閱讀

Async函數規格草案出奇的短,而且應該能成爲一個有趣的讀物, 若是你熱衷於學習更多這些即將到來的功能。

我已經粘貼了一段代碼在下面, 它是爲了幫助你理解async函數的內部是如何工做的。即便咱們不能夠填充新的關鍵字,它也能夠幫助你理解在async/await的帷幕後面發生了什麼事情。

「換句話說,它應該對學習異步函數內部原理很是有幫助,不管是對生成器仍是promise。」

首先,下面的一小段代碼展現了一個async函數如何經過常規的function關鍵字來簡化聲明過程,這將返回一個生成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會被代碼包裝起來並傳入生成器函數中,經過用戶代碼串行的執行,並將值傳遞到你的「生成器」代碼中(async函數的函數體中)。 在這個意義上,咱們能夠注意Async函數真的是生成器和primose對象之上的語法糖,這對於讓你理解其中每個環節是如何工做來講很是重要,這是爲了讓你對於混合、匹配、合併不一樣的異步代碼流的寫法有一個更好的理解。

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) { // 執行失敗,並拒絕promise對象 reject(e); return; } if (next.done) { // 執行成功,處理promise對象 resolve(next.value); return; } // 未完成,以yield標記的promise對象唄中斷,並在此執行step方法 Promise.resolve(next.value).then( v => step(() => gen.next(v)), e => step(() => gen.throw(e)) ); } }); }
相關文章
相關標籤/搜索