# Promise 對象
## 概述
Promise 對象是 JavaScript 的異步操作解決方案,為異步操作提供統一接口。它起到代理作用(proxy),充當異步操作與回調函數之間的中介,使得異步操作具備同步操作的接口。Promise 可以讓異步操作寫起來,就像在寫同步操作的流程,而不必一層層地嵌套回調函數。
注意,本章只是 Promise 對象的簡單介紹。為了避免與后續教程的重復,更完整的介紹請看[《ES6 標準入門》](http://es6.ruanyifeng.com/)的[《Promise 對象》](http://es6.ruanyifeng.com/#docs/promise)一章。
首先,Promise 是一個對象,也是一個構造函數。
```javascript
function f1(resolve, reject) {
// 異步代碼...
}
var p1 = new Promise(f1);
```
上面代碼中,`Promise`構造函數接受一個回調函數`f1`作為參數,`f1`里面是異步操作的代碼。然后,返回的`p1`就是一個 Promise 實例。
Promise 的設計思想是,所有異步任務都返回一個 Promise 實例。Promise 實例有一個`then`方法,用來指定下一步的回調函數。
```javascript
var p1 = new Promise(f1);
p1.then(f2);
```
上面代碼中,`f1`的異步操作執行完成,就會執行`f2`。
傳統的寫法可能需要把`f2`作為回調函數傳入`f1`,比如寫成`f1(f2)`,異步操作完成后,在`f1`內部調用`f2`。Promise 使得`f1`和`f2`變成了鏈式寫法。不僅改善了可讀性,而且對于多層嵌套的回調函數尤其方便。
```javascript
// 傳統寫法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promise 的寫法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
```
從上面代碼可以看到,采用 Promises 以后,程序流程變得非常清楚,十分易讀。注意,為了便于理解,上面代碼的`Promise`實例的生成格式,做了簡化,真正的語法請參照下文。
總的來說,傳統的回調函數寫法使得代碼混成一團,變得橫向發展而不是向下發展。Promise 就是解決這個問題,使得異步流程可以寫成同步流程。
Promise 原本只是社區提出的一個構想,一些函數庫率先實現了這個功能。ECMAScript 6 將其寫入語言標準,目前 JavaScript 原生支持 Promise 對象。
## Promise 對象的狀態
Promise 對象通過自身的狀態,來控制異步操作。Promise 實例具有三種狀態。
- 異步操作未完成(pending)
- 異步操作成功(fulfilled)
- 異步操作失敗(rejected)
上面三種狀態里面,`fulfilled`和`rejected`合在一起稱為`resolved`(已定型)。
這三種的狀態的變化途徑只有兩種。
- 從“未完成”到“成功”
- 從“未完成”到“失敗”
一旦狀態發生變化,就凝固了,不會再有新的狀態變化。這也是 Promise 這個名字的由來,它的英語意思是“承諾”,一旦承諾成效,就不得再改變了。這也意味著,Promise 實例的狀態變化只可能發生一次。
因此,Promise 的最終結果只有兩種。
- 異步操作成功,Promise 實例傳回一個值(value),狀態變為`fulfilled`。
- 異步操作失敗,Promise 實例拋出一個錯誤(error),狀態變為`rejected`。
## Promise 構造函數
JavaScript 提供原生的`Promise`構造函數,用來生成 Promise 實例。
```javascript
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 異步操作成功 */){
resolve(value);
} else { /* 異步操作失敗 */
reject(new Error());
}
});
```
上面代碼中,`Promise`構造函數接受一個函數作為參數,該函數的兩個參數分別是`resolve`和`reject`。它們是兩個函數,由 JavaScript 引擎提供,不用自己實現。
`resolve`函數的作用是,將`Promise`實例的狀態從“未完成”變為“成功”(即從`pending`變為`fulfilled`),在異步操作成功時調用,并將異步操作的結果,作為參數傳遞出去。`reject`函數的作用是,將`Promise`實例的狀態從“未完成”變為“失敗”(即從`pending`變為`rejected`),在異步操作失敗時調用,并將異步操作報出的錯誤,作為參數傳遞出去。
下面是一個例子。
```javascript
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100)
```
上面代碼中,`timeout(100)`返回一個 Promise 實例。100毫秒以后,該實例的狀態會變為`fulfilled`。
## Promise.prototype.then()
Promise 實例的`then`方法,用來添加回調函數。
`then`方法可以接受兩個回調函數,第一個是異步操作成功時(變為`fulfilled`狀態)的回調函數,第二個是異步操作失敗(變為`rejected`)時的回調函數(該參數可以省略)。一旦狀態改變,就調用相應的回調函數。
```javascript
var p1 = new Promise(function (resolve, reject) {
resolve('成功');
});
p1.then(console.log, console.error);
// "成功"
var p2 = new Promise(function (resolve, reject) {
reject(new Error('失敗'));
});
p2.then(console.log, console.error);
// Error: 失敗
```
上面代碼中,`p1`和`p2`都是Promise 實例,它們的`then`方法綁定兩個回調函數:成功時的回調函數`console.log`,失敗時的回調函數`console.error`(可以省略)。`p1`的狀態變為成功,`p2`的狀態變為失敗,對應的回調函數會收到異步操作傳回的值,然后在控制臺輸出。
`then`方法可以鏈式使用。
```javascript
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
```
上面代碼中,`p1`后面有四個`then`,意味依次有四個回調函數。只要前一步的狀態變為`fulfilled`,就會依次執行緊跟在后面的回調函數。
最后一個`then`方法,回調函數是`console.log`和`console.error`,用法上有一點重要的區別。`console.log`只顯示`step3`的返回值,而`console.error`可以顯示`p1`、`step1`、`step2`、`step3`之中任意一個發生的錯誤。舉例來說,如果`step1`的狀態變為`rejected`,那么`step2`和`step3`都不會執行了(因為它們是`resolved`的回調函數)。Promise 開始尋找,接下來第一個為`rejected`的回調函數,在上面代碼中是`console.error`。這就是說,Promise 對象的報錯具有傳遞性。
## then() 用法辨析
Promise 的用法,簡單說就是一句話:使用`then`方法添加回調函數。但是,不同的寫法有一些細微的差別,請看下面四種寫法,它們的差別在哪里?
```javascript
// 寫法一
f1().then(function () {
return f2();
});
// 寫法二
f1().then(function () {
f2();
});
// 寫法三
f1().then(f2());
// 寫法四
f1().then(f2);
```
為了便于講解,下面這四種寫法都再用`then`方法接一個回調函數`f3`。寫法一的`f3`回調函數的參數,是`f2`函數的運行結果。
```javascript
f1().then(function () {
return f2();
}).then(f3);
```
寫法二的`f3`回調函數的參數是`undefined`。
```javascript
f1().then(function () {
f2();
return;
}).then(f3);
```
寫法三的`f3`回調函數的參數,是`f2`函數返回的函數的運行結果。
```javascript
f1().then(f2())
.then(f3);
```
寫法四與寫法一只有一個差別,那就是`f2`會接收到`f1()`返回的結果。
```javascript
f1().then(f2)
.then(f3);
```
## 實例:圖片加載
下面是使用 Promise 完成圖片的加載。
```javascript
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
```
上面代碼中,`image`是一個圖片對象的實例。它有兩個事件監聽屬性,`onload`屬性在圖片加載成功后調用,`onerror`屬性在加載失敗調用。
上面的`preloadImage()`函數用法如下。
```javascript
preloadImage('https://example.com/my.jpg')
.then(function (e) { document.body.append(e.target) })
.then(function () { console.log('加載成功') })
```
上面代碼中,圖片加載成功以后,`onload`屬性會返回一個事件對象,因此第一個`then()`方法的回調函數,會接收到這個事件對象。該對象的`target`屬性就是圖片加載后生成的 DOM 節點。
## 小結
Promise 的優點在于,讓回調函數變成了規范的鏈式寫法,程序流程可以看得很清楚。它有一整套接口,可以實現許多強大的功能,比如同時執行多個異步操作,等到它們的狀態都改變以后,再執行一個回調函數;再比如,為多個回調函數中拋出的錯誤,統一指定處理方法等等。
而且,Promise 還有一個傳統寫法沒有的好處:它的狀態一旦改變,無論何時查詢,都能得到這個狀態。這意味著,無論何時為 Promise 實例添加回調函數,該函數都能正確執行。所以,你不用擔心是否錯過了某個事件或信號。如果是傳統寫法,通過監聽事件來執行回調函數,一旦錯過了事件,再添加回調函數是不會執行的。
Promise 的缺點是,編寫的難度比傳統寫法高,而且閱讀代碼也不是一眼可以看懂。你只會看到一堆`then`,必須自己在`then`的回調函數里面理清邏輯。
## 微任務
Promise 的回調函數屬于異步任務,會在同步任務之后執行。
```javascript
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
```
上面代碼會先輸出2,再輸出1。因為`console.log(2)`是同步任務,而`then`的回調函數屬于異步任務,一定晚于同步任務執行。
但是,Promise 的回調函數不是正常的異步任務,而是微任務(microtask)。它們的區別在于,正常任務追加到下一輪事件循環,微任務追加到本輪事件循環。這意味著,微任務的執行時間一定早于正常任務。
```javascript
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
```
上面代碼的輸出結果是`321`。這說明`then`的回調函數的執行時間,早于`setTimeout(fn, 0)`。因為`then`是本輪事件循環執行,`setTimeout(fn, 0)`在下一輪事件循環開始時執行。
## 參考鏈接
- Sebastian Porto, [Asynchronous JS: Callbacks, Listeners, Control Flow Libs and Promises](http://sporto.github.com/blog/2012/12/09/callbacks-listeners-promises/)
- Rhys Brett-Bowen, [Promises/A+ - understanding the spec through implementation](http://modernjavascript.blogspot.com/2013/08/promisesa-understanding-by-doing.html)
- Matt Podwysocki, Amanda Silver, [Asynchronous Programming in JavaScript with “Promises”](http://blogs.msdn.com/b/ie/archive/2011/09/11/asynchronous-programming-in-javascript-with-promises.aspx)
- Marc Harter, [Promise A+ Implementation](https://gist.github.com//wavded/5692344)
- Bryan Klimt, [What’s so great about JavaScript Promises?](http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/)
- Jake Archibald, [JavaScript Promises There and back again](http://www.html5rocks.com/en/tutorials/es6/promises/)
- Mikito Takada, [7. Control flow, Mixu's Node book](http://book.mixu.net/node/ch7.html)
- 前言
- 入門篇
- 導論
- 歷史
- 基本語法
- 數據類型
- 概述
- null,undefined 和布爾值
- 數值
- 字符串
- 對象
- 函數
- 數組
- 運算符
- 算術運算符
- 比較運算符
- 布爾運算符
- 二進制位運算符
- 其他運算符,運算順序
- 語法專題
- 數據類型的轉換
- 錯誤處理機制
- 編程風格
- console 對象與控制臺
- 標準庫
- Object 對象
- 屬性描述對象
- Array 對象
- 包裝對象
- Boolean 對象
- Number 對象
- String 對象
- Math 對象
- Date 對象
- RegExp 對象
- JSON 對象
- 面向對象編程
- 實例對象與 new 命令
- this 關鍵字
- 對象的繼承
- Object 對象的相關方法
- 嚴格模式
- 異步操作
- 概述
- 定時器
- Promise 對象
- DOM
- 概述
- Node 接口
- NodeList 接口,HTMLCollection 接口
- ParentNode 接口,ChildNode 接口
- Document 節點
- Element 節點
- 屬性的操作
- Text 節點和 DocumentFragment 節點
- CSS 操作
- Mutation Observer API
- 事件
- EventTarget 接口
- 事件模型
- Event 對象
- 鼠標事件
- 鍵盤事件
- 進度事件
- 表單事件
- 觸摸事件
- 拖拉事件
- 其他常見事件
- GlobalEventHandlers 接口
- 瀏覽器模型
- 瀏覽器模型概述
- window 對象
- Navigator 對象,Screen 對象
- Cookie
- XMLHttpRequest 對象
- 同源限制
- CORS 通信
- Storage 接口
- History 對象
- Location 對象,URL 對象,URLSearchParams 對象
- ArrayBuffer 對象,Blob 對象
- File 對象,FileList 對象,FileReader 對象
- 表單,FormData 對象
- IndexedDB API
- Web Worker
- 附錄:網頁元素接口
- a
- img
- form
- input
- button
- option
- video,audio