Promise 是什麼 Promise是非同步編程的一種解決方案。Promise對象表示了非同步操作的最終狀態(完成或失敗)和返回的結果。 其實我們在jQuery的ajax中已經見識了部分Promise的實現,通過Promise,我們能夠將回調轉換為鏈式調用,也起到解耦的作用。 怎麼用 Promise接 ...
Promise
是什麼
Promise是非同步編程的一種解決方案。Promise對象表示了非同步操作的最終狀態(完成或失敗)和返回的結果。
其實我們在jQuery的ajax中已經見識了部分Promise的實現,通過Promise,我們能夠將回調轉換為鏈式調用,也起到解耦的作用。
怎麼用
Promise介面的基本思想是讓非同步操作返回一個Promise對象
三種狀態和兩種變化途徑
Promise對象只有三種狀態。
- 非同步操作“未完成”(pending)
- 非同步操作“已完成”(resolved,又稱fulfilled)
- 非同步操作“失敗”(rejected)
這三種的狀態的變化途徑只有兩種。
- 非同步操作從“未完成”到“已完成”
- 非同步操作從“未完成”到“失敗”。
這種變化只能發生一次,一旦當前狀態變為“已完成”或“失敗”,就意味著不會再有新的狀態變化了。因此,Promise對象的最終結果只有兩種。
非同步操作成功,Promise對象傳回一個值,狀態變為resolved。
非同步操作失敗,Promise對象拋出一個錯誤,狀態變為rejected。
生成Promise對象
通過new Promise來生成Promise對象:
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
then和catch回調
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.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1, p2, p3])
上面代碼中,Promise.all方法接受一個數組作為參數,p1、p2、p3都是Promise對象的實例,如果不是,就會先調用下麵講到的Promise.resolve方法,將參數轉為Promise實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有Iterator介面,且返回的每個成員都是Promise實例。)
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成resolved,p的狀態才會變成resolved,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被Rejected,p的狀態就變成Rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
Promise.race()
與Promise.all()類似,不過是只要有一個Promise實例先改變了狀態,p的狀態就是它的狀態,傳遞給回調函數的結果也是它的結果。所以很形象地叫做賽跑。
Promise.resolve()和Promise.reject()
有時需要將現有對象轉為Promise對象,可以使用這兩個方法。
Generator(生成器)
是什麼
生成器本質上是一種特殊的迭代器(參見本文章系列二之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遍歷
生成器部署了迭代器介面,因此可以用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後面只能跟迭代器,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函數的含義:編譯器的"傳名調用"實現,往往是將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫做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對象的自動執行
首先,將方法包裝成一個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模塊
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(非同步)函數
是什麼
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