目錄:
1.概述
相信大家都聽過Node中著名的回撥地獄(callback hell)。因為Node中的操作預設都是非同步執行的,所以需要呼叫者傳入一個回撥函式以便在操作結束時進行相應的處理。當回撥的層次變多,程式碼就變得越來越難以編寫、理解和閱讀。
Promise
是ES6中新增的一種非同步程式設計的方式,用於解決回撥的方式的各種問題,提供了更多的可能性。其實早在ES6之前,社群就已經有多種Promise
的實現方式了:
以上幾種Promise
庫都遵循Promise/A+規範。ES6也採用了該規範,所以這些實現的API都是類似的,可以相互對照學習。
Promise
表示的是一個計算結果或網路請求的佔位符。由於當前計算或網路請求尚未完成,所以結果暫時無法取得。
Promise
物件一共有3中狀態,pending
,fullfilled
(又稱為resolved
)和rejected
:
pending
——任務仍在進行中。resolved
——任務已完成。reject
——任務出錯。
Promise
物件初始時處於pending
狀態,其生命週期內只可能發生以下一種狀態轉換:
- 任務完成,狀態由
pending
轉換為resolved
。 - 任務出錯返回,狀態由
pending
轉換為rejected
。
Promise
物件的狀態轉換一旦發生,就不可再次更改。這或許就是Promise
之“承諾”的含義吧。
2.基本用法
2.1 建立Promise
Javascript
提供了Promise
建構函式用於建立Promise
物件。格式如下:
let p = new Promise(executor(resolve, reject));
複製程式碼
程式碼中executor
是使用者自定義的函式,用於實現具體非同步操作流程。該函式有兩個引數resolve
和reject
,它們是Javascript引擎提供的函式,不需要使用者實現。在executor
函式中,如果非同步操作成功,則呼叫resolve
將Promise
的狀態轉換為resolved
,resolve
函式以結果資料作為引數。如果非同步操作失敗,則呼叫reject
將Promise
的狀態轉換為rejected
,reject
函式以具體錯誤物件作為引數。
2.2 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
複製程式碼
2.3 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
複製程式碼
2.4 其他建立Promise
物件的方式
除了Promise
建構函式,ES6還提供了兩個簡單易用的建立Promise
物件的方式,即Promise.resolve
和Promise.reject
。
Promise.resolve
顧名思義,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
Promise.reject
不管引數為何種型別,都是建立一個狀態為rejected
的Promise
物件。
3.高階用法
3.1 Flatten 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
。
3.2 Promise.all 方法
很多時候,我們想要等待多個非同步操作完成後再進行一些處理。如果使用回撥的方式,會出現前面提到過的回撥地獄。例如:
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);
});
複製程式碼
3.3 Promise.race 方法
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
複製程式碼
4.使用案例
前面我們提到過,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:。
5.總結
Promise
是ES6新增的一種非同步程式設計的解決方案,使用它可以編寫更優雅,更易讀,更易維護的程式。Promise
已經應用在各個角落了,個人認為掌握它是一個合格的Javascript開發者的基本功。
6.參考連結
Tasks, microtasks, queues and schedules
An Overview of JavaScript Promise
ES6 Promise:Promise語法介紹
Promise 物件:阮一峰老師Promise物件詳解