含義 Promise 是非同步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了 Promise 對象。 Promise 對象是一個代理對象(代理一個值),被代理的值在 Promise 對象創建時可能是未 ...
含義
Promise 是非同步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了 Promise 對象。
Promise 對象是一個代理對象(代理一個值),被代理的值在 Promise 對象創建時可能是未知的。它允許你為非同步操作的成功和失敗分別綁定相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的 Promise 對象。
Promise 對象有以下兩個特點:
- 對象的狀態不受外界影響。Promise 對象代表一個非同步操作,有三種狀態:
pending(進行中)
、fulfilled(已成功)
和rejected(已失敗)
。只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。這也是Promise這個名字的由來,它的英語意思就是“承諾”,表示其他手段無法改變。 - 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。Promise 對象的狀態改變,只有兩種可能:
從 pending 變為 fulfilled
和從 pending 變為 rejected
。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為resolved(已定型)
。如果改變已經發生了,你再對 Promise 對象添加回調函數,也會立即得到這個結果。這與事件(Event)完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
基本用法
new Promise( function(resolve, reject) {...} /* executor */ );
Promise 對象的初始化接收一個執行函數 executor,executor 是帶有 resolve 和 reject 兩個參數的函數 。
Promise 構造函數執行時會立即調用 executor 函數, resolve 和 reject 兩個函數作為參數傳遞給 executor(executor 函數在 Promise 構造函數返回新建對象前被調用)。
resolve 和 reject 函數被調用時,分別將 promise 的狀態改為 fulfilled(完成)
或 rejected(失敗)
。executor 內部通常會執行一些非同步操作,一旦完成,可以調用 resolve 函數來將 promise 狀態改成 fulfilled
,或者在發生錯誤時將它的狀態改為 rejected
。
如果在 executor 函數中拋出一個錯誤,那麼該 promise 狀態為 rejected
。executor函數的返回值被忽略。
先看個示例:(註:後文的示例均使用 setTimeout 模擬非同步操作)
// 從 pending 變為 fulfilled
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
resolve('promise fulfilled!');
}, 500);
}).then(function(data) {
console.log(data);
});
// Hi,
// promise fulfilled!
// 從 pending 變為 rejected
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
reject('promise rejected!');
}, 500);
}).then(null, function(error) {
console.log(error);
});
// Hi,
// promise rejected!
解釋一下 從 pending 變為 fulfilled
這段代碼,當執行 new Promise() 時,傳入的執行函數就立即執行了,此時其內部有一個非同步操作(過 500ms 之後執行),等過了 500ms 之後先執行 console.log('Hi,');
輸出 Hi,
,此時 promise 的狀態為 pending(進行中)
,而執行 resolve('Promise!');
則修改 promise 的狀態為 fulfilled(完成)
,然後我們調用 then()
接收 promise 在 fulfilled
狀態下傳遞的值,此時輸出 'Promise!'
。
同理,從 pending 變為 rejected
這段代碼基本差不多,不同的是非同步操作調用了 reject
方法,then
方法使用第二個參數接收 rejected
狀態下傳遞的值。
Promise.prototype.then()
then 的作用是為 Promise 實例添加狀態改變時的回調函數。
then 方法的第一個參數是 resolved 狀態的回調函數,第二個參數(可選)是 rejected 狀態的回調函數。
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('promise fulfilled!');
} else {
reject('promise rejected!');
}
}, 500);
}).then(function(data) {
console.log(data);
}, function(error) {
console.log(error);
});
// 如果調用 resolve 方法,輸出如下:
// Hi,
// promise fulfilled!
// 如果調用 reject 方法,輸出如下:
// Hi,
// promise rejected!
then 方法返回的是一個新的 Promise 實例(註意,不是原來那個Promise實例)。因此可以採用鏈式寫法,即 then 方法後面再調用另一個 then 方法。採用鏈式的 then,可以指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的還是一個 Promise 對象(即有非同步操作),這時後一個回調函數,就會等待該 Promise 對象的狀態發生變化,才會被調用。
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
resolve();
}, 500);
}).then(function() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('promise fulfilled!');
} else {
reject('promise rejected!');
}
});
})
}).then(function(data) {
console.log(data);
}, function(error) {
console.log(error);
});
// 如果第一個 then 調用 resolve 方法,第二個 then 調用第一個回調函數,最終輸出如下:
// Hi,
// promise fulfilled!
// 如果第一個 then 調用 reject 方法,第二個 then 調用第一個回調函數,最終輸出如下:
// Hi,
// promise rejected!
Promise.prototype.catch()
catch 方法是.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
所以下麵代碼:
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('promise fulfilled!');
} else {
reject('promise rejected!');
}
}, 500);
}).then(function(data) {
console.log(data);
}, function(error) {
console.log(error);
});
等價於:
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('promise fulfilled!');
} else {
reject('promise rejected!');
}
}, 500);
}).then(function(data) {
console.log(data);
}).catch(function(error) {
console.log(error);
});
如果沒有使用 catch 方法或者 then 第二個參數指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應,這跟傳統的 try/catch 代碼塊是不同。
catch 方法返回的還是一個 Promise 對象,因此後面還可以接著調用 then 方法。
catch 方法與 .then(null, rejection) 的不同:
- 如果非同步操作拋出錯誤,狀態就會變為 rejected,就會調用 catch 方法指定的回調函數,處理這個錯誤。
- then 方法指定的回調函數,如果運行中拋出錯誤,也會被 catch 方法捕獲。
- catch 方法的寫法更接近同步的寫法(try/catch)。
因此,建議總是使用 catch 方法,而不使用 then 方法的第二個參數。
Promise.prototype.finally()
finally 方法用於指定不管 Promise 對象最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('Hi,');
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('promise fulfilled!');
} else {
reject('promise rejected!');
}
}, 500);
}).then(function(data) {
console.log(data);
}).catch(function(error) {
console.log(error);
}).finally(function() {
console.log('I am finally!');
});
上面代碼中,不管 promise 最後的狀態,在執行完 then 或 catch 指定的回調函數以後,都會執行 finally 方法指定的回調函數。
Promise.all()
Promise.all 方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。
var p = Promise.all([p1, p2]);
上面代碼中,Promise.all 方法接受一個數組作為參數,p1、p2 都是 Promise 實例,如果不是,就會先調用下麵講到的 Promise.resolve 方法,將參數轉為 Promise 實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 實例。)
p的狀態由p1、p2決定,分成兩種情況。
(1)只有 p1、p2 的狀態都變成 fulfilled,p 的狀態才會變成 fulfilled,此時 p1、p2 的返回值組成一個數組,傳遞給 p 的回調函數。
(2)只要 p1、p2 之中有一個被 rejected,p 的狀態就變成 rejected,此時第一個被 reject 的實例的返回值,會傳遞給 p 的回調函數。
示例:
試想一個頁面聊天系統,我們需要從兩個不同的 URL 分別獲得用戶的個人信息和好友列表,這兩個任務是可以並行執行的,用Promise.all()實現。
// 並行執行非同步任務
var p1 = new Promise(function (resolve, reject) {
setTimeout(function() {
// 模擬請求,請求狀態為200代表成功,不是200代表失敗
if (status === 200) {
resolve('P1');
} else {
reject('error');
}
}, 500);
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同時執行p1和p2,併在它們都完成後執行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 輸出:['P1', 'P2']
}).catch(function(error) {
console.log(error); // 如果p1執行失敗,則輸出:error
});
註意,如果作為參數的 Promise 實例,自己定義了 catch 方法,那麼它一旦被 rejected,並不會觸發 Promise.all() 的 catch 方法。
Promise.race()
Promise.race 方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
var p = Promise.race([p1, p2]);
上面代碼中,只要 p1、p2 之中有一個實例率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調函數。
Promise.race 方法的參數與 Promise.all 方法一樣,如果不是 Promise 實例,就會先調用下麵講到的 Promise.resolve 方法,將參數轉為 Promise 實例,再進一步處理。
示例:
有些時候,多個非同步任務是為了容錯。比如,同時向兩個 URL 讀取用戶的個人信息,只需要獲得先返回的結果即可。這種情況下,用Promise.race()實現。
// 多任務容錯
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 400, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P2'
});
Promise.resolve()
有時需要將現有對象轉為 Promise 對象,Promise.resolve 方法就起到這個作用。
Promise.resolve方法的參數分成四種情況:
(1)參數是一個 Promise 實例
如果參數是 Promise 實例,那麼Promise.resolve將不做任何修改、原封不動地返回這個實例。
(2)參數是一個 thenable 對象
thenable 對象指的是具有 then 方法的對象,比如下麵這個對象。
var thenable = {
then: function (resolve, reject) {
resolve(42);
}
};
Promise.resolve 方法會將這個對象轉為 Promise 對象,然後就立即執行 thenable 對象的 then 方法。
var thenable = {
then: function (resolve, reject) {
resolve(42);
}
};
var p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
上面代碼中,thenable 對象的 then 方法執行後,對象 p1 的狀態就變為 resolved,從而立即執行最後那個 then 方法指定的回調函數,輸出 42。
(3)參數不是具有 then 方法的對象,或根本就不是對象
如果參數是一個原始值,或者是一個不具有 then 方法的對象,則 Promise.resolve 方法返回一個新的 Promise 對象,狀態為 resolved。
var p = Promise.resolve('Hello');
p.then(function (s) {
console.log(s)
});
// 'Hello'
var p1 = Promise.resolve(true);
p1.then(function (b) {
console.log(b)
});
// true
var p2 = Promise.resolve(1);
p1.then(function (n) {
console.log(n)
});
// 1
(4)不帶有任何參數
Promise.resolve 方法允許調用時不帶參數,直接返回一個 resolved 狀態的 Promise 對象。
所以,如果希望得到一個 Promise 對象,比較方便的方法就是直接調用 Promise.resolve 方法。
Promise.reject()
Promise.reject 方法也會返回一個新的 Promise 實例,該實例的狀態為 rejected。
註意,Promise.reject 方法的參數,會原封不動地作為 reject 的參數,變成後續方法的參數。這一點與 Promise.resolve 方法不一致。
var thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e = > {
console.log(e === thenable)
})
// true
上面代碼中,Promise.reject 方法的參數是一個 thenable 對象,執行以後,後面 catch 方法的參數不是 reject 拋出的 出錯了
這個字元串,而是 thenable 對象。
應用
載入圖片
我們可以將圖片的載入寫成一個 Promise,一旦載入完成,Promise 的狀態就發生變化。
function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
封裝ajax
我們可以將 ajax 請求寫成一個 Promise,根據請求的不同狀態改變 Promise 的狀態。
function ajax(method, url, data) {
var request = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status === 200) {
resolve(request.responseText);
} else {
reject(request.status);
}
}
};
request.open(method, url);
request.send(data);
});
}
總結
優點:
- 可以將非同步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數(
回調地獄
)。 - 在非同步執行的流程中,可以把執行代碼和處理結果的代碼清晰地分離開來。
缺點:
- 無法取消 Promise,一旦新建它就會立即執行,無法中途取消。
- 如果不設置回調函數,Promise 內部拋出的錯誤,不會反應到外部。
- 當處於
pending
狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
參考資料
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
http://es6.ruanyifeng.com/#docs/promise