Promise是異步編程的一種解決方案。Promise對象表示了異步操做的最終狀態(完成或失敗)和返回的結果。html
其實咱們在jQuery的ajax中已經見識了部分Promise的實現,經過Promise,咱們可以將回調轉換爲鏈式調用,也起到解耦的做用。node
Promise接口的基本思想是讓異步操做返回一個Promise對象git
Promise對象只有三種狀態。github
這三種的狀態的變化途徑只有兩種。ajax
這種變化只能發生一次,一旦當前狀態變爲「已完成」或「失敗」,就意味着不會再有新的狀態變化了。所以,Promise對象的最終結果只有兩種。shell
異步操做成功,Promise對象傳回一個值,狀態變爲resolved。編程
異步操做失敗,Promise對象拋出一個錯誤,狀態變爲rejected。json
經過new Promise來生成Promise對象:api
var promise = new Promise(function(resolve, reject) { // 異步操做的代碼 if (/* 異步操做成功 */){ resolve(value) } else { reject(error) } })
Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject。它們是兩個函數,由JavaScript引擎提供,不用本身部署。數組
resolve會將Promise對象的狀態從pending變爲resolved,reject則是將Promise對象的狀態從pending變爲rejected。
Promise構造函數接受一個函數後會當即執行這個函數
var promise = new Promise(function () { console.log('Hello World') }) // Hello World
Promise對象生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。then方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。第二個函數是可選的。分別稱之爲成功回調和失敗回調。成功回調接收異步操做成功的結果爲參數,失敗回調接收異步操做失敗報出的錯誤做爲參數。
var promise = new Promise(function (resolve, reject) { setTimeout(function () { resolve('成功') }, 3000) }) promise.then(function (data){ console.log(data) }) // 3s後打印'成功'
catch方法是then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
var promise = new Promise(function (resolve, reject) { setTimeout(function () { reject('失敗') }, 3000) }) promise.catch(function (data){ console.log(data) }) // 3s後打印'失敗'
Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1, p2, p3])
上面代碼中,Promise.all方法接受一個數組做爲參數,p一、p二、p3都是Promise對象的實例,若是不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲Promise實例,再進一步處理。(Promise.all方法的參數能夠不是數組,但必須具備Iterator接口,且返回的每一個成員都是Promise實例。)
p的狀態由p一、p二、p3決定,分紅兩種狀況。
(1)只有p一、p二、p3的狀態都變成resolved,p的狀態纔會變成resolved,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p一、p二、p3之中有一個被Rejected,p的狀態就變成Rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
與Promise.all()相似,不過是隻要有一個Promise實例先改變了狀態,p的狀態就是它的狀態,傳遞給回調函數的結果也是它的結果。因此很形象地叫作賽跑。
有時須要將現有對象轉爲Promise對象,可使用這兩個方法。
生成器本質上是一種特殊的迭代器(參見本文章系列二之Iterator)。ES6裏的迭代器並非一種新的語法或者是新的內置對象(構造函數),而是一種協議 (protocol)。全部遵循了這個協議的對象均可以稱之爲迭代器對象。生成器對象由生成器函數返回而且遵照了迭代器協議。具體參見MDN。
生成器函數的語法爲function*,在其函數體內部可使用yield和yield*關鍵字。
function* gen(x){ console.log(1) var y = yield x + 2 console.log(2) return y } var g = gen(1)
當咱們像上面那樣調用生成器函數時,會發現並無輸出。這就是生成器函數與普通函數的不一樣,它能夠交出函數的執行權(即暫停執行)。yield表達式就是暫停標誌。
以前提到了生成器對象遵循迭代器協議,因此其實能夠經過next方法執行。執行結果也是一個包含value和done屬性的對象。
遍歷器對象的next方法的運行邏輯以下。
(1)遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。
(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。
(3)若是沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,做爲返回的對象的value屬性值。
(4)若是該函數沒有return語句,則返回的對象的value屬性值爲undefined。
須要注意的是,yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。
g.next() // 1 // { value: 3, done: false } g.next() // 2 // { value: undefined, done: true }
生成器部署了迭代器接口,所以能夠用for...of來遍歷,不用調用next方法
function *foo() { yield 1 yield 2 yield 3 return 4 } for (let v of foo()) { console.log(v) } // 1 // 2 // 3
從語法角度看,若是yield表達式後面跟的是一個遍歷器對象,須要在yield表達式後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield表達式。yield後面只能跟迭代器,yield*的功能是將迭代控制權交給後面的迭代器,達到遞歸迭代的目的
function* foo() { yield 'a' yield 'b' } function* bar() { yield 'x' yield* foo() yield 'y' } for (let v of bar()) { console.log(v) } // x // a // b // y
下面是使用Generator函數執行一個真實的異步任務的例子:
var fetch = require('node-fetch') function* gen () { var url = 'https://api.github.com/users/github' var result = yield fetch(url) console.log(result.bio) }
上面代碼中,Generator函數封裝了一個異步操做,該操做先讀取一個遠程接口,而後從JSON格式的數據解析信息。這段代碼很是像同步操做,除了加上了yield命令。
執行這段代碼的方法以下
var g = gen() var result = g.next() result .value .then(function (data) { return data.json() }) .then(function (data) { g.next(data) })
上面代碼中,首先執行Generator函數,獲取遍歷器對象,而後使用next方法(第二行),執行異步任務的第一階段。因爲Fetch模塊返回的是一個Promise對象,所以要用then方法調用下一個next方法。
能夠看到,雖然Generator函數將異步操做表示得很簡潔,可是流程管理卻不方便(即什麼時候執行第一階段、什麼時候執行第二階段)。
那麼如何自動化異步任務的流程管理呢?
Generator函數就是一個異步操做的容器。它的自動執行須要一種機制,當異步操做有告終果,可以自動交回執行權。
兩種方法能夠作到這一點。
回調函數。將異步操做包裝成Thunk函數,在回調函數裏面交回執行權。
Promise對象。將異步操做包裝成Promise對象,用then方法交回執行權。
本節很簡略,可能會看不太明白,請參考Thunk 函數的含義和用法
Thunk函數的含義:編譯器的"傳名調用"實現,每每是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫作Thunk函數。
JavaScript語言是傳值調用,它的Thunk函數含義有所不一樣。在JavaScript語言中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成單參數的版本,且只接受回調函數做爲參數。
任何函數,只要參數有回調函數,就能寫成Thunk函數的形式,能夠經過一個Thunk函數轉換器來轉換。
Thunk函數真正的威力,在於能夠自動執行Generator函數。咱們能夠實現一個基於Thunk函數的Generator執行器,而後直接把Generator函數傳入這個執行器便可。
function run(fn) { var gen = fn() function next(err, data) { var result = gen.next(data) if (result.done) return result.value(next) } next() } function* g() { // ... } run(g)
Thunk函數並非Generator函數自動執行的惟一方案。由於自動執行的關鍵是,必須有一種機制,自動控制Generator函數的流程,接收和交還程序的執行權。回調函數能夠作到這一點,Promise對象也能夠作到這一點。
首先,將方法包裝成一個Promise對象(fs是nodejs的一個內置模塊)。
var fs = require('fs') var readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function (error, data) { if (error) reject(error) resolve(data) }) }) } var gen = function* () { var f1 = yield readFile('/etc/fstab') var f2 = yield readFile('/etc/shells') console.log(f1.toString()) console.log(f2.toString()) }
而後,手動執行上面的Generator函數。
var g = gen() g.next().value.then(function (data) { g.next(data).value.then(function (data) { g.next(data) }) })
觀察上面的執行過程,實際上是在遞歸調用,咱們能夠用一個函數來實現:
function run(gen){ var g = gen() function next(data){ var result = g.next(data) if (result.done) return result.value result.value.then(function(data){ next(data) }) } next() } run(gen)
上面代碼中,只要Generator函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。
co模塊是nodejs社區著名的TJ大神寫的一個小工具,用於Generator函數的自動執行。
下面是一個Generator函數,用於依次讀取兩個文件
var gen = function* () { var f1 = yield readFile('/etc/fstab') var f2 = yield readFile('/etc/shells') console.log(f1.toString()) console.log(f2.toString()) } var co = require('co') co(gen)
co模塊可讓你不用編寫Generator函數的執行器。Generator函數只要傳入co函數,就會自動執行。co函數返回一個Promise對象,所以能夠用then方法添加回調函數。
co(gen).then(function () { console.log('Generator 函數執行完成') })
co模塊的原理:其實就是將兩種自動執行器(Thunk函數和Promise對象),包裝成一個模塊。使用co的前提條件是,Generator函數的yield命令後面,只能是Thunk函數或Promise對象。若是數組或對象的成員,所有都是Promise對象,也可使用co(co v4.0版之後,yield命令後面只能是Promise對象,再也不支持Thunk函數)。
async函數屬於ES7。目前,它仍處於提案階段,可是轉碼器Babel和regenerator都已經支持。async函數能夠說是目前異步操做最好的解決方案,是對Generator函數的升級和改進。
1)語法
async函數聲明定義了異步函數,它會返回一個AsyncFunction對象。和普通函數同樣,你也能夠定義一個異步函數表達式。
調用異步函數時會返回一個promise對象。當這個異步函數成功返回一個值時,將會使用promise的resolve方法來處理這個返回值,當異步函數拋出的是異常或者非法值時,將會使用promise的reject方法來處理這個異常值。
異步函數可能會包括await表達式,這將會使異步函數暫停執行並等待promise解析傳值後,繼續執行異步函數並返回解析值。
注意:await只能用在async函數中。
前面依次讀取兩個文件的代碼寫成async函數以下:
var asyncReadFile = async function (){ var f1 = await readFile('/etc/fstab') var f2 = await readFile('/etc/shells') console.log(f1.toString()) console.log(f2.toString()) }
async函數將Generator函數的星號(*)替換成了async,將yield改成了await。
2)async函數的改進
async函數對Generator函數的改進,體如今如下三點。
(1)內置執行器。Generator函數的執行必須靠執行器,因此纔有了co函數庫,而async函數自帶執行器。也就是說,async函數的執行,與普通函數如出一轍,只要一行。
var result = asyncReadFile()
(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
(3)更廣的適用性。co函數庫約定,yield命令後面只能是Thunk函數或Promise對象,而async函數的await命令後面,能夠跟Promise對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操做)。
3)基本用法
同Generator函數同樣,async函數返回一個Promise對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。
function resolveAfter2Seconds (x) { return new Promise(resolve => { setTimeout(() => { resolve(x) }, 2000) }) } async function add1 (x) { var a = resolveAfter2Seconds(20) var b = resolveAfter2Seconds(30) return x + await a + await b } add1(10).then(v => { console.log(v) }) // 2s後打印60 async function add2 (x) { var a = await resolveAfter2Seconds(20) var b = await resolveAfter2Seconds(30) return x + a + b } add2(10).then(v => { console.log(v) }) // 4s後打印60
4)捕獲錯誤
可使用.catch回調捕獲錯誤,也可使用傳統的try...catch。
async function myFunction () { try { await somethingThatReturnsAPromise() } catch (err) { console.log(err) } } // 另外一種寫法 async function myFunction () { await somethingThatReturnsAPromise() .catch(function (err) { console.log(err) } }
5)併發的異步操做
let foo = await getFoo() let bar = await getBar()
多個await命令後面的異步操做會按順序完成。若是不存在繼發關係,最好讓它們同時觸發。上面的代碼只有getFoo完成,纔會去執行getBar,這樣會比較耗時。若是這兩個是獨立的異步操做,徹底可讓它們同時觸發。
// 寫法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]) // 寫法二 let fooPromise = getFoo() let barPromise = getBar() let foo = await fooPromise let bar = await barPromise