目錄:
1.概述
相信大家都聽過Node中著名的回調(diào)地獄(callback hell)。因?yàn)镹ode中的操作默認(rèn)都是異步執(zhí)行的,所以需要調(diào)用者傳入一個(gè)回調(diào)函數(shù)以便在操作結(jié)束時(shí)進(jìn)行相應(yīng)的處理。當(dāng)回調(diào)的層次變多,代碼就變得越來越難以編寫、理解和閱讀。
Promise是ES6中新增的一種異步編程的方式,用于解決回調(diào)的方式的各種問題,提供了更多的可能性。其實(shí)早在ES6之前,社區(qū)就已經(jīng)有多種Promise的實(shí)現(xiàn)方式了:
以上幾種Promise庫(kù)都遵循Promise/A+規(guī)范。ES6也采用了該規(guī)范,所以這些實(shí)現(xiàn)的API都是類似的,可以相互對(duì)照學(xué)習(xí)。
Promise表示的是一個(gè)計(jì)算結(jié)果或網(wǎng)絡(luò)請(qǐng)求的占位符。由于當(dāng)前計(jì)算或網(wǎng)絡(luò)請(qǐng)求尚未完成,所以結(jié)果暫時(shí)無法取得。
Promise對(duì)象一共有3中狀態(tài),pending,fullfilled(又稱為resolved)和rejected:
-
pending——任務(wù)仍在進(jìn)行中。 -
resolved——任務(wù)已完成。 -
reject——任務(wù)出錯(cuò)。
Promise對(duì)象初始時(shí)處于pending狀態(tài),其生命周期內(nèi)只可能發(fā)生以下一種狀態(tài)轉(zhuǎn)換:
- 任務(wù)完成,狀態(tài)由
pending轉(zhuǎn)換為resolved。 - 任務(wù)出錯(cuò)返回,狀態(tài)由
pending轉(zhuǎn)換為rejected。
Promise對(duì)象的狀態(tài)轉(zhuǎn)換一旦發(fā)生,就不可再次更改。這或許就是Promise之“承諾”的含義吧。
2.基本用法
2.1 創(chuàng)建Promise
Javascript提供了Promise構(gòu)造函數(shù)用于創(chuàng)建Promise對(duì)象。格式如下:
let p = new Promise(executor(resolve, reject));
代碼中executor是用戶自定義的函數(shù),用于實(shí)現(xiàn)具體異步操作流程。該函數(shù)有兩個(gè)參數(shù)resolve和reject,它們是Javascript引擎提供的函數(shù),不需要用戶實(shí)現(xiàn)。在executor函數(shù)中,如果異步操作成功,則調(diào)用resolve將Promise的狀態(tài)轉(zhuǎn)換為resolved,resolve函數(shù)以結(jié)果數(shù)據(jù)作為參數(shù)。如果異步操作失敗,則調(diào)用reject將Promise的狀態(tài)轉(zhuǎn)換為rejected,reject函數(shù)以具體錯(cuò)誤對(duì)象作為參數(shù)。
2.2 then方法
Promise對(duì)象創(chuàng)建完成之后,我們需要調(diào)用then(succ_handler, fail_handler)方法指定成功和/或失敗的回調(diào)處理。例如:
let p = new Promise(function(resolve, reject) {
resolve("finished");
});
p.then(function (data) {
console.log(data); // 輸出finished
}, function (err) {
console.log("oh no, ", err.message);
});
在上面的代碼中,我們創(chuàng)建了一個(gè)Promise對(duì)象,在executor函數(shù)中調(diào)用resolve將該對(duì)象狀態(tài)轉(zhuǎn)換為resolved。
進(jìn)而then指定的成功回調(diào)函數(shù)被調(diào)用,輸出finished。
let p = new Promise(function(resolve, reject) {
reject(new Error("something be wrong"));
});
p.then(function (data) {
console.log(data);
}, function (err) {
console.log("oh no, ", err); // 輸出oh no, something be wrong
});
以上代碼中,在executor函數(shù)中調(diào)用reject將Promise對(duì)象狀態(tài)轉(zhuǎn)換為rejected。
進(jìn)而then指定的失敗回調(diào)函數(shù)被調(diào)用,輸出oh no, something be wrong。
這就是最基本的使用Promise編寫異步處理的方式了。但是,有幾點(diǎn)需要注意:
(1) then方法可以只傳入成功或失敗回調(diào)。
(2)executor函數(shù)是立即執(zhí)行的,而成功或失敗的回調(diào)函數(shù)會(huì)到當(dāng)前EventLoop的最后再執(zhí)行。下面的代碼可以驗(yàn)證這一點(diǎn):
let p = new Promise(function(resolve, reject) {
console.log("promise constructor");
resolve("finished");
});
p.then(function (data) {
console.log(data);
});
console.log("end");
輸出結(jié)果為:
promise constructor
end
finished
(3) then方法返回的是一個(gè)新的Promise對(duì)象,所以可以鏈?zhǔn)秸{(diào)用:
let p = new Promise(function(resolve) {
resolve(5);
});
p.then(function (data) {
return data * 2;
})
.then(function (data) {
console.log(data); // 輸出10
});
(4)Promise對(duì)象的then方法可以被調(diào)用多次,而且可以被重復(fù)調(diào)用(不同于事件,同一個(gè)事件的回調(diào)只會(huì)被調(diào)用一次。)。
let p = new Promise(function(resolve) {
resolve("repeat");
});
p.then(function (data) {
console.log(data);
});
p.then(function (data) {
console.log(data);
});
p.then(function (data) {
console.log(data);
});
輸出:
repeat
repeat
repeat
2.3 catch方法
由前面的介紹,我們知道,可以由then方法指定錯(cuò)誤處理。但是ES6提供了一個(gè)更好用的方法catch。直觀上理解可以認(rèn)為catch(handler)等同于then(null, handler)。
let p = new Promise(function(resolve, reject) {
reject(new Error("something be wrong"));
});
p.catch(function (err) {
console.log("oh no, ", err.message); // 輸出oh no, something be wrong
});
通常不建議在then方法中指定錯(cuò)誤處理,而是在調(diào)用鏈的最后增加一個(gè)catch方法用于處理前面的步驟中出現(xiàn)的錯(cuò)誤。
使用時(shí)注意一下幾點(diǎn):
then方法指定兩個(gè)處理函數(shù),調(diào)用成功處理函數(shù)拋出異常時(shí),失敗處理函數(shù)不會(huì)被調(diào)用。Promise中未被處理的異常不會(huì)終止當(dāng)前的執(zhí)行流程,也就是說Promise會(huì)“吞掉異?!?/strong>。
let p = new Promise(function (resolve, reject) {
throw new Error("something be wrong");
});
p.then(function (data) {
console.log(data);
});
console.log("end");
// 程序正常結(jié)束,輸出end
2.4 其他創(chuàng)建Promise對(duì)象的方式
除了Promise構(gòu)造函數(shù),ES6還提供了兩個(gè)簡(jiǎn)單易用的創(chuàng)建Promise對(duì)象的方式,即Promise.resolve和Promise.reject。
Promise.resolve
顧名思義,Promise.resolve創(chuàng)建一個(gè)resolved狀態(tài)的Promise對(duì)象:
let p = Promise.resolve("hello");
p.then(function (data) {
console.log(data); // 輸出hello
});
Promise.resolve的參數(shù)分為以下幾種類型:
(1)參數(shù)是一個(gè)Promise對(duì)象,那么直接返回該對(duì)象。
(2) 參數(shù)是一個(gè)thenable對(duì)象,即擁有then函數(shù)的對(duì)象。這時(shí)Promise.resolve會(huì)將該對(duì)象轉(zhuǎn)換為一個(gè)Promise對(duì)象,并且立即執(zhí)行其then函數(shù)。
let thenable = {
then: function (resolve, reject) {
resolve(25);
};
};
let p = Promise.resolve(thenable);
p.then(function (data) {
console.log(data); // 輸出25
});
(3)其他參數(shù)(無參數(shù)相當(dāng)于有一個(gè)undefined參數(shù)),創(chuàng)建一個(gè)狀態(tài)為resolved的Promise對(duì)象,參數(shù)作為操作結(jié)果會(huì)傳遞給后續(xù)回調(diào)處理。
Promise.reject
Promise.reject不管參數(shù)為何種類型,都是創(chuàng)建一個(gè)狀態(tài)為rejected的Promise對(duì)象。
3.高級(jí)用法
3.1 Flatten Promise
then方法的成功回調(diào)函數(shù)可以返回一個(gè)新的Promise對(duì)象,這時(shí)舊的Promise對(duì)象將會(huì)被凍結(jié),其狀態(tài)取決于新Promise對(duì)象的狀態(tài)。
let p1 = new Promise(function (resolve) {
setTimeout(function () {
resolve("promise1");
}, 3000);
});
let p2 = new Promise(function (resolve) {
resolve("promise2");
});
p2.then(function (data) {
return p1; // (A)
})
.then(function (data) { // (B)
console.log(data); // 輸出promise2
});
我們?cè)?A)行直接返回了另一個(gè)Promise對(duì)象。后面的then方法執(zhí)行取決于該對(duì)象的狀態(tài),所以在3s后輸出promise1,不會(huì)輸出promise2。
3.2 Promise.all 方法
很多時(shí)候,我們想要等待多個(gè)異步操作完成后再進(jìn)行一些處理。如果使用回調(diào)的方式,會(huì)出現(xiàn)前面提到過的回調(diào)地獄。例如:
let fs = require("fs");
fs.readFile("file1", "utf8", function (data1, err1) {
if (err1 != nil) {
console.log(err1);
return;
}
fs.readFile("file2", "utf8", function (data2, err2) {
if (err2 != nil) {
console.log(err2);
return;
}
fs.readFile("file3", "utf8", function (data3, err3) {
if (err3 != nil) {
console.log(err3);
return;
}
console.log(data1);
console.log(data2);
console.log(data3);
});
});
});
假設(shè)文件file1,file2,file3中的內(nèi)容分別是"in file1","in file2","in file3"。那么輸出如下:
in file1
in file2
in file3
這種情況下,Promise.all就派上大用場(chǎng)了。Promise.all接受一個(gè)可迭代對(duì)象(即ES6中的Iterable對(duì)象),每個(gè)元素通過調(diào)用Promise.resolve轉(zhuǎn)換為Promise對(duì)象。Promise.all方法返回一個(gè)新的Promise對(duì)象。該對(duì)象在所有Promise對(duì)象狀態(tài)變?yōu)?code>resolved時(shí),其狀態(tài)才會(huì)轉(zhuǎn)換為resolved,參數(shù)為各個(gè)Promise的結(jié)果組成的數(shù)組。只要有一個(gè)對(duì)象的狀態(tài)變?yōu)?code>rejected,新對(duì)象的狀態(tài)就會(huì)轉(zhuǎn)換為rejected。使用Promise.all我們可以很優(yōu)雅的實(shí)現(xiàn)上面的功能:
let fs = require("fs");
let promise1 = new Promise(function (resolve, reject) {
fs.readFile("file1", "utf8", function (err, data) {
if (err != null) {
reject(err);
} else {
resolve(data);
}
});
});
let promise2 = new Promise(function (resolve, reject) {
fs.readFile("file2", "utf8", function (err, data) {
if (err != null) {
reject(err);
} else {
resolve(data);
}
});
});
let promise3 = new Promise(function (resolve, reject) {
fs.readFile("file3", "utf8", function (err, data) {
if (err != null) {
reject(err);
} else {
resolve(data);
}
});
});
let p = Promise.all([promise1, promise2, promise3]);
p.then(function (datas) {
console.log(datas);
})
.catch(function (err) {
console.log(err);
});
輸出如下:
['in file1', 'in file2', 'in file3']
第二段代碼我們可以進(jìn)一步簡(jiǎn)化為:
let fs = require("fs");
let myReadFile = function (filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, "utf8", function (err, data) {
if (err != null) {
reject(err);
} else {
resolve(data);
}
});
});
}
let promise1 = myReadFile("file1");
let promise2 = myReadFile("file2");
let promise3 = myReadFile("file3");
let p = Promise.all([promise1, promise2, promise3]);
p.then(function (datas) {
console.log(datas);
})
.catch(function (err) {
console.log(err);
});
3.3 Promise.race 方法
Promise.race與Promise.all一樣,接受一個(gè)可迭代對(duì)象作為參數(shù),返回一個(gè)新的Promise對(duì)象。不同的是,只要參數(shù)中有一個(gè)Promise對(duì)象狀態(tài)發(fā)生變化,新對(duì)象的狀態(tài)就會(huì)變化。也就是說哪個(gè)操作快,就用哪個(gè)結(jié)果(或出錯(cuò))。利用這種特性,我們可以實(shí)現(xiàn)超時(shí)處理:
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
reject(new Error("time out"));
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
// 模擬耗時(shí)操作
setTimeout(function () {
resolve("get result");
}, 2000);
});
let p = Promise.race([p1, p2]);
p.then(function (data) {
console.log(data);
})
.catch(function (err) {
console.log(err);
});
對(duì)象p1在1s之后狀態(tài)轉(zhuǎn)換為rejected,p2在2s后轉(zhuǎn)換為resolved。所以1s后,p1狀態(tài)轉(zhuǎn)換時(shí),p的狀態(tài)緊接著就轉(zhuǎn)為rejected了。從而,輸出為:
time out
如果將對(duì)象p2的延遲改為0.5s,那么在0.5s后p2狀態(tài)改變時(shí),p緊隨其后狀態(tài)轉(zhuǎn)換為resolved。從而輸出為:
get result
4.使用案例
前面我們提到過,then方法會(huì)返回一個(gè)新的Promise對(duì)象。所以then方法可以鏈?zhǔn)秸{(diào)用,前一個(gè)成功回調(diào)的返回值會(huì)作為下一個(gè)成功回調(diào)的參數(shù)。例如:
let p = new Promise(function (resolve, reject) {
resolve(25);
});
p.then(function (num) { // (A)
return num + 1;
})
.then(function (num) { // (B)
return num * 2;
})
.then(function (num) { // (C)
console.log(num);
});
對(duì)象p狀態(tài)變?yōu)?code>resolved時(shí),結(jié)果為25。行(A)處函數(shù)最先被調(diào)用,參數(shù)num的值為25,返回值為26。26又作為行(B)處函數(shù)的參數(shù),函數(shù)返回52。52作為行(C)處函數(shù)的參數(shù),被輸出。
下面給出結(jié)合AJAX的一個(gè)案例。
let getJSON = function (url) {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status === 200) {
resolve(xhr.response);
} else {
reject(new Error(xhr.statusText));
}
}
xhr.send();
});
}
getJSON("http://api.icndb.com/jokes/random")
.then(function (responseText) {
return JSON.parse(responseText);
})
.then(function (obj) {
console.log(obj.value.joke);
})
.catch(function (err) {
console.log(err.message);
});
getJSON函數(shù)接受一個(gè)url地址,請(qǐng)求json數(shù)據(jù)。但是請(qǐng)求到的數(shù)據(jù)是文本格式,所以在第一個(gè)then方法的回調(diào)中使用JSON.parse將其轉(zhuǎn)為對(duì)象,第二個(gè)then方法回調(diào)再進(jìn)行具體處理。
http://api.icndb.com/jokes/random是一個(gè)隨機(jī)笑話的api,大家可以試試 :smile:。
5.總結(jié)
Promise是ES6新增的一種異步編程的解決方案,使用它可以編寫更優(yōu)雅,更易讀,更易維護(hù)的程序。Promise已經(jīng)應(yīng)用在各個(gè)角落了,個(gè)人認(rèn)為掌握它是一個(gè)合格的Javascript開發(fā)者的基本功。
6.參考鏈接
Tasks, microtasks, queues and schedules
An Overview of JavaScript Promise
ES6 Promise:Promise語(yǔ)法介紹
Promise 對(duì)象:阮一峰老師Promise對(duì)象詳解
關(guān)于我:
個(gè)人主頁(yè) 簡(jiǎn)書 掘金