Promise 對象
轉載:http://wiki.jikexueyuan.com/project/es6/promise.html
基本用法
ES6 原生提供了 Promise 對象。所謂 Promise 對象,就是代表了某個未來才會知道結果的事件(通常是一個異步操作),並且這個事件提供統一的 API,可供進一步處理。
有了 Promise 對象,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise 對象提供的接口,使得控制異步操作更加容易。Promise 對象的概念的詳細解釋,請參考《JavaScript標准參考教程》。
ES6 的 Promise 對象是一個構造函數,用來生成 Promise 實例。
var promise = new Promise(function(resolve, reject) {
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
上面代碼中,Promise 構造函數接受一個函數作為參數,該函數的兩個參數分別是 resolve 方法和 reject 方法。如果異步操作成功,則用 resolve 方法將 Promise 對象的狀態,從“未完成”變為“成功”(即從 pending 變為 resolved);如果異步操作失敗,則用 reject 方法將 Promise 對象的狀態,從“未完成”變為“失敗”(即從 pending 變為 rejected)。
Promise 實例生成以后,可以用 then 方法分別指定 resolve 方法和 reject 方法的回調函數。
下面是一個使用 Promise 對象的簡單例子。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
timeout(100).then(() => {
console.log('done');
});
上面代碼中,timeout 方法返回一個 Promise 實例,表示一段時間以后才會發生的結果。一旦 Promise 對象的狀態變為 resolved,就會觸發 then 方法綁定的回調函數。
下面是一個用 Promise 對象實現的 Ajax 操作的例子。
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出錯了', error);
});
上面代碼中,getJSON 是對 XMLHttpRequest 對象的封裝,用於發出一個針對 JSON 數據的 HTTP 請求,並且返回一個 Promise 對象。需要注意的是,在 getJSON 內部,resolve 方法和 reject 方法調用時,都帶有參數。
如果調用 resolve 方法和 reject 方法時帶有參數,那么它們的參數會被傳遞給回調函數。reject 方法的參數通常是 Error 對象的實例,表示拋出的錯誤;resolve 方法的參數除了正常的值以外,還可能是另一個 Promise 實例,表示異步操作的結果有可能是一個值,也有可能是另一個異步操作,比如像下面這樣。
var p1 = new Promise(function(resolve, reject){
// ...
});
var p2 = new Promise(function(resolve, reject){
// ...
resolve(p1);
})
上面代碼中,p1 和 p2 都是 Promise 的實例,但是 p2 的 resolve 方法將 p1 作為參數,p1 的狀態就會傳遞給 p2。
注意,這時 p1 的狀態決定了 p2 的狀態。如果 p1 的狀態是 pending,那么 p2 的回調函數就會等待 p1 的狀態改變;如果 p1 的狀態已經是 fulfilled 或者 rejected,那么 p2 的回調函數將會立刻執行。
Promise.prototype.then()
Promise.prototype.then 方法返回的是一個新的Promise對象,因此可以采用鏈式寫法,即then方法后面再調用另一個then方法。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成以后,會將返回結果作為參數,傳入第二個回調函數。
如果前一個回調函數返回的是Promise對象,這時后一個回調函數就會等待該Promise對象有了運行結果,才會進一步調用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// ...
});
then方法還可以接受第二個參數,表示Promise對象的狀態變為rejected時的回調函數。
Promise.prototype.catch()
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
getJSON("/posts.json").then(function(posts) {
// ...
}).catch(function(error) {
// 處理前一個回調函數運行時發生的錯誤
console.log('發生錯誤!', error);
});
上面代碼中,getJSON方法返回一個Promise對象,如果該對象運行正常,則會調用then方法指定的回調函數;如果該方法拋出錯誤,則會調用catch方法指定的回調函數,處理這個錯誤。
下面是一個例子。
var promise = new Promise(function(resolve, reject) {
throw new Error('test')
});
promise.catch(function(error) { console.log(error) });
// Error: test
上面代碼中,Promise拋出一個錯誤,就被catch方法指定的回調函數捕獲。
如果Promise狀態已經變成resolved,再拋出錯誤是無效的。
var promise = new Promise(function(resolve, reject) {
resolve("ok");
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代碼中,Promise在resolve語句后面,再拋出錯誤,不會被捕獲,等於沒有拋出。
Promise對象的錯誤具有“冒泡”性質,會一直向后傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 處理前面三個Promise產生的錯誤
});
上面代碼中,一共有三個Promise對象:一個由getJSON產生,兩個由then產生。它們之中任何一個拋出的錯誤,都會被最后一個catch捕獲。
跟傳統的try/catch代碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回調函數,Promise對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
上面代碼中,someAsyncThing函數產生的Promise對象會報錯,但是由於沒有調用catch方法,這個錯誤不會被捕獲,也不會傳遞到外層代碼,導致運行后沒有任何輸出。
var promise = new Promise(function(resolve, reject) {
resolve("ok");
setTimeout(function() { throw new Error('test') }, 0)
});
promise.then(function(value) { console.log(value) });
// ok
// Uncaught Error: test
上面代碼中,Promise指定在下一輪“事件循環”再拋出錯誤,結果由於沒有指定catch語句,就冒泡到最外層,成了未捕獲的錯誤。
Node.js有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤。
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});
上面代碼中,unhandledRejection事件的監聽函數有兩個參數,第一個是錯誤對象,第二個是報錯的Promise實例,它可以用來了解發生錯誤的環境信息。。
需要注意的是,catch方法返回的還是一個Promise對象,因此后面還可以接着調用then方法。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代碼運行完catch方法指定的回調函數,會接着運行后面那個then方法指定的回調函數。
catch方法之中,還能再拋出錯誤。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因為x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,因為y沒有聲明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
上面代碼中,catch方法拋出一個錯誤,因為后面沒有別的catch方法了,導致這個錯誤不會被捕獲,也不會到傳遞到外層。如果改寫一下,結果就不一樣了。
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行會報錯,因為y沒有聲明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
上面代碼中,第二個catch方法用來捕獲,前一個catch方法拋出的錯誤。
Promise.all(),Promise.race()
Promise.all方法用於將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.all([p1,p2,p3]);
上面代碼中,Promise.all方法接受一個數組作為參數,p1、p2、p3都是Promise對象的實例。(Promise.all方法的參數不一定是數組,但是必須具有iterator接口,且返回的每個成員都是Promise實例。)
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
下面是一個具體的例子。
// 生成一個Promise對象的數組
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function(posts) {
// ...
}).catch(function(reason){
// ...
});
Promise.race方法同樣是將多個Promise實例,包裝成一個新的Promise實例。
var p = Promise.race([p1,p2,p3]);
上面代碼中,只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的Promise實例的返回值,就傳遞給p的返回值。
如果Promise.all方法和Promise.race方法的參數,不是Promise實例,就會先調用下面講到的Promise.resolve方法,將參數轉為Promise實例,再進一步處理。
Promise.resolve(),Promise.reject()
有時需要將現有對象轉為Promise對象,Promise.resolve方法就起到這個作用。
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼將jQuery生成deferred對象,轉為一個新的ES6的Promise對象。
如果Promise.resolve方法的參數,不是具有then方法的對象(又稱thenable對象),則返回一個新的Promise對象,且它的狀態為fulfilled。
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代碼生成一個新的Promise對象的實例p,它的狀態為fulfilled,所以回調函數會立即執行,Promise.resolve方法的參數就是回調函數的參數。
所以,如果希望得到一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。
var p = Promise.resolve();
p.then(function () {
// ...
});
上面代碼的變量p就是一個Promise對象。
如果Promise.resolve方法的參數是一個Promise對象的實例,則會被原封不動地返回。
Promise.reject(reason)方法也會返回一個新的Promise實例,該實例的狀態為rejected。Promise.reject方法的參數reason,會被傳遞給實例的回調函數。
var p = Promise.reject('出錯了');
p.then(null, function (s){
console.log(s)
});
// 出錯了
上面代碼生成一個Promise對象的實例p,狀態為rejected,回調函數會立即執行。
Generator函數與Promise的結合
使用Generator函數管理流程,遇到異步操作的時候,通常返回一個Promise對象。
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
var g = function* () {
try {
var foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
var it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(value));
});
}
go(it.next());
}
run(g);
上面代碼的Generator函數g之中,有一個異步操作getFoo,它返回的就是一個Promise對象。函數run用來處理這個Promise對象,並調用下一個next方法。
async函數
概述
async函數與Promise、Generator函數一樣,是用來取代回調函數、解決異步操作的一種方法。它本質上是Generator函數的語法糖。async函數並不屬於ES6,而是被列入了ES7,但是traceur、Babel.js、regenerator等轉碼器已經支持這個功能,轉碼后立刻就能使用。
下面是一個Generator函數,依次讀取兩個文件。
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());
};
上面代碼中,readFile函數是fs.readFile的Promise版本。
寫成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,僅此而已。
async函數對Generator函數的改進,體現在以下三點。
(1)內置執行器。Generator函數的執行必須靠執行器,而async函數自帶執行器。也就是說,async函數的執行,與普通函數一模一樣,只要一行。
var result = asyncReadFile();
(2)更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。
(3)更廣的適用性。co函數庫約定,yield命令后面只能是Thunk函數或Promise對象,而async函數的await命令后面,可以跟Promise對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操作)。
實現
async函數的實現,就是將Generator函數和自動執行器,包裝在一個函數里。
async function fn(args){
// ...
}
// 等同於
function fn(args){
return spawn(function*() {
// ...
});
}
所有的async函數都可以寫成上面的第二種形式,其中的spawn函數就是自動執行器。
下面給出spawn函數的實現,基本就是前文自動執行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
用法
同Generator函數一樣,async函數返回一個Promise對象,可以使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到觸發的異步操作完成,再接着執行函數體內后面的語句。
下面是一個例子。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
上面代碼是一個獲取股票報價的函數,函數前面的async關鍵字,表明該函數內部有異步操作。調用該函數時,會立即返回一個Promise對象。
上面的例子用Generator函數表達,就是下面這樣。
function getStockPriceByName(name) {
return spawn(function*(name) {
var symbol = yield getStockSymbol(name);
var stockPrice = yield getStockPrice(symbol);
return stockPrice;
});
}
上面的例子中,spawn函數是一個自動執行器,由JavaScript引擎內置。它的參數是一個Generator函數。async...await結構本質上,是在語言層面提供的異步任務的自動執行器。
下面是一個更一般性的例子,指定多少毫秒后輸出一個值。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
上面代碼指定50毫秒以后,輸出“hello world”。
注意點
await命令后面的Promise對象,運行結果可能是rejected,所以最好把await命令放在try...catch代碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一種寫法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
};
}
await命令只能用在async函數之中,如果用在普通函數,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
上面代碼可能不會正常工作,原因是這時三個db.post操作將是並發執行,也就是同時執行,而不是繼發執行。正確的寫法是采用for循環。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
如果確實希望多個請求並發執行,可以使用Promise.all方法。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的寫法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
ES6將await增加為保留字。使用這個詞作為標識符,在ES5是合法的,在ES6將拋出SyntaxError。
與Promise、Generator的比較
我們通過一個例子,來看Async函數與Promise、Generator函數的區別。
假定某個DOM元素上面,部署了一系列的動畫,前一個動畫結束,才能開始后一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執行的動畫的返回值。
首先是Promise的寫法。
function chainAnimationsPromise(elem, animations) { // 變量ret用來保存上一個動畫的返回值 var ret = null; // 新建一個空的Promise var p = Promise.resolve(); // 使用then方法,添加所有動畫 for(var anim in animations) { p = p.then(function(val) { ret = val; return anim(elem); }) } // 返回一個部署了錯誤捕捉機制的Promise return p.catch(function(e) { /* 忽略錯誤,繼續執行 */ }).then(function() { return ret; }); }
雖然Promise的寫法比回調函數的寫法大大改進,但是一眼看上去,代碼完全都是Promise的API(then、catch等等),操作本身的語義反而不容易看出來。
接着是Generator函數的寫法。
function chainAnimationsGenerator(elem, animations) { return spawn(function*() { var ret = null; try { for(var anim of animations) { ret = yield anim(elem); } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }); }
上面代碼使用Generator函數遍歷了每個動畫,語義比Promise寫法更清晰,用戶定義的操作全部都出現在spawn函數的內部。這個寫法的問題在於,必須有一個任務運行器,自動執行Generator函數,上面代碼的spawn函數就是自動執行器,它返回一個Promise對象,而且必須保證yield語句后面的表達式,必須返回一個Promise。
最后是Async函數的寫法。
async function chainAnimationsAsync(elem, animations) { var ret = null; try { for(var anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }
可以看到Async函數的實現最簡潔,最符合語義,幾乎沒有語義不相關的代碼。它將Generator寫法中的自動執行器,改在語言層面提供,不暴露給用戶,因此代碼量最少。如果使用Generator寫法,自動執行器需要用戶自己提供。
