相信你們都聽過Node中著名的回調地獄(callback hell)。由於Node中的操做默認都是異步執行的,因此須要調用者傳入一個回調函數以便在操做結束時進行相應的處理。當回調的層次變多,代碼就變得愈來愈難以編寫、理解和閱讀。javascript
Promise
是ES6中新增的一種異步編程的方式,用於解決回調的方式的各類問題,提供了更多的可能性。其實早在ES6以前,社區就已經有多種Promise
的實現方式了:java
以上幾種Promise
庫都遵循Promise/A+規範。ES6也採用了該規範,因此這些實現的API都是相似的,能夠相互對照學習。git
Promise
表示的是一個計算結果或網絡請求的佔位符。因爲當前計算或網絡請求還沒有完成,因此結果暫時沒法取得。es6
Promise
對象一共有3中狀態,pending
,fullfilled
(又稱爲resolved
)和rejected
:github
pending
——任務仍在進行中。resolved
——任務已完成。reject
——任務出錯。Promise
對象初始時處於pending
狀態,其生命週期內只可能發生如下一種狀態轉換:web
pending
轉換爲resolved
。pending
轉換爲rejected
。Promise
對象的狀態轉換一旦發生,就不可再次更改。這或許就是Promise
之「承諾」的含義吧。編程
Promise
Javascript
提供了Promise
構造函數用於建立Promise
對象。格式以下:json
let p = new Promise(executor(resolve, reject));
複製代碼
代碼中executor
是用戶自定義的函數,用於實現具體異步操做流程。該函數有兩個參數resolve
和reject
,它們是Javascript引擎提供的函數,不須要用戶實現。在executor
函數中,若是異步操做成功,則調用resolve
將Promise
的狀態轉換爲resolved
,resolve
函數以結果數據做爲參數。若是異步操做失敗,則調用reject
將Promise
的狀態轉換爲rejected
,reject
函數以具體錯誤對象做爲參數。api
then
方法Promise
對象建立完成以後,咱們須要調用then(succ_handler, fail_handler)
方法指定成功和/或失敗的回調處理。例如:數組
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);
});
複製代碼
在上面的代碼中,咱們建立了一個Promise
對象,在executor
函數中調用resolve
將該對象狀態轉換爲resolved
。
進而then
指定的成功回調函數被調用,輸出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
函數中調用reject
將Promise
對象狀態轉換爲rejected
。
進而then
指定的失敗回調函數被調用,輸出oh no, something be wrong
。
這就是最基本的使用Promise
編寫異步處理的方式了。可是,有幾點須要注意:
(1) then
方法能夠只傳入成功或失敗回調。
(2)executor
函數是當即執行的,而成功或失敗的回調函數會到當前EventLoop
的最後再執行。下面的代碼能夠驗證這一點:
let p = new Promise(function(resolve, reject) {
console.log("promise constructor");
resolve("finished");
});
p.then(function (data) {
console.log(data);
});
console.log("end");
複製代碼
輸出結果爲:
promise constructor
end
finished
複製代碼
(3) then
方法返回的是一個新的Promise
對象,因此能夠鏈式調用:
let p = new Promise(function(resolve) {
resolve(5);
});
p.then(function (data) {
return data * 2;
})
.then(function (data) {
console.log(data); // 輸出10
});
複製代碼
(4)Promise
對象的then
方法能夠被調用屢次,並且能夠被重複調用(不一樣於事件,同一個事件的回調只會被調用一次。)。
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
複製代碼
catch
方法由前面的介紹,咱們知道,能夠由then
方法指定錯誤處理。可是ES6提供了一個更好用的方法catch
。直觀上理解能夠認爲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
方法中指定錯誤處理,而是在調用鏈的最後增長一個catch
方法用於處理前面的步驟中出現的錯誤。
使用時注意一下幾點:
then
方法指定兩個處理函數,調用成功處理函數拋出異常時,失敗處理函數不會被調用。
Promise
中未被處理的異常不會終止當前的執行流程,也就是說Promise
會**「吞掉異常」**。
let p = new Promise(function (resolve, reject) {
throw new Error("something be wrong");
});
p.then(function (data) {
console.log(data);
});
console.log("end");
// 程序正常結束,輸出end
複製代碼
Promise
對象的方式除了Promise
構造函數,ES6還提供了兩個簡單易用的建立Promise
對象的方式,即Promise.resolve
和Promise.reject
。
顧名思義,Promise.resolve
建立一個resolved
狀態的Promise
對象:
let p = Promise.resolve("hello");
p.then(function (data) {
console.log(data); // 輸出hello
});
複製代碼
Promise.resolve
的參數分爲如下幾種類型:
(1)參數是一個Promise
對象,那麼直接返回該對象。
(2) 參數是一個thenable
對象,即擁有then
函數的對象。這時Promise.resolve
會將該對象轉換爲一個Promise
對象,而且當即執行其then
函數。
let thenable = {
then: function (resolve, reject) {
resolve(25);
};
};
let p = Promise.resolve(thenable);
p.then(function (data) {
console.log(data); // 輸出25
});
複製代碼
(3)其餘參數(無參數至關於有一個undefined參數),建立一個狀態爲resolved
的Promise
對象,參數做爲操做結果會傳遞給後續回調處理。
Promise.reject
無論參數爲什麼種類型,都是建立一個狀態爲rejected
的Promise
對象。
then
方法的成功回調函數能夠返回一個新的Promise
對象,這時舊的Promise
對象將會被凍結,其狀態取決於新Promise
對象的狀態。
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
});
複製代碼
咱們在(A)行直接返回了另外一個Promise
對象。後面的then
方法執行取決於該對象的狀態,因此在3s後輸出promise1
,不會輸出promise2
。
不少時候,咱們想要等待多個異步操做完成後再進行一些處理。若是使用回調的方式,會出現前面提到過的回調地獄。例如:
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);
});
});
});
複製代碼
假設文件file1
,file2
,file3
中的內容分別是"in file1","in file2","in file3"。那麼輸出以下:
in file1
in file2
in file3
複製代碼
這種狀況下,Promise.all
就派上大用場了。Promise.all
接受一個可迭代對象(即ES6中的Iterable對象),每一個元素經過調用Promise.resolve
轉換爲Promise
對象。Promise.all
方法返回一個新的Promise
對象。該對象在全部Promise
對象狀態變爲resolved
時,其狀態纔會轉換爲resolved
,參數爲各個Promise
的結果組成的數組。只要有一個對象的狀態變爲rejected
,新對象的狀態就會轉換爲rejected
。使用Promise.all
咱們能夠很優雅的實現上面的功能:
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']
複製代碼
第二段代碼咱們能夠進一步簡化爲:
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);
});
複製代碼
Promise.race
與Promise.all
同樣,接受一個可迭代對象做爲參數,返回一個新的Promise
對象。不一樣的是,只要參數中有一個Promise
對象狀態發生變化,新對象的狀態就會變化。也就是說哪一個操做快,就用哪一個結果(或出錯)。利用這種特性,咱們能夠實現超時處理:
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
reject(new Error("time out"));
}, 1000);
});
let p2 = new Promise(function (resolve, reject) {
// 模擬耗時操做
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);
});
複製代碼
對象p1
在1s以後狀態轉換爲rejected
,p2
在2s後轉換爲resolved
。因此1s後,p1
狀態轉換時,p
的狀態緊接着就轉爲rejected
了。從而,輸出爲:
time out
複製代碼
若是將對象p2
的延遲改成0.5s,那麼在0.5s後p2
狀態改變時,p
緊隨其後狀態轉換爲resolved
。從而輸出爲:
get result
複製代碼
前面咱們提到過,then
方法會返回一個新的Promise
對象。因此then
方法能夠鏈式調用,前一個成功回調的返回值會做爲下一個成功回調的參數。例如:
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);
});
複製代碼
對象p
狀態變爲resolved
時,結果爲25
。行(A)處函數最早被調用,參數num
的值爲25
,返回值爲26
。26
又做爲行(B)處函數的參數,函數返回52
。52
做爲行(C)處函數的參數,被輸出。
下面給出結合AJAX的一個案例。
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
函數接受一個url
地址,請求json數據。可是請求到的數據是文本格式,因此在第一個then
方法的回調中使用JSON.parse
將其轉爲對象,第二個then
方法回調再進行具體處理。
http://api.icndb.com/jokes/random
是一個隨機笑話的api,你們能夠試試 :smile:。
Promise
是ES6新增的一種異步編程的解決方案,使用它能夠編寫更優雅,更易讀,更易維護的程序。Promise
已經應用在各個角落了,我的認爲掌握它是一個合格的Javascript開發者的基本功。
Tasks, microtasks, queues and schedules
An Overview of JavaScript Promise
ES6 Promise:Promise語法介紹
Promise 對象:阮一峯老師Promise對象詳解