一.Promise的含義和意義
Promise是抽象非同步處理物件以及對其進行各種操作的元件,其實Promise就是一個物件,用來傳遞非同步操作的訊息,它不是某門語言特有的屬性,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件,Promise物件有以下兩個特點:
1.物件的狀態不受外界影響
2.一旦狀態改變,就不會再變,任何時候都可以得到這個結果
Promise也以下缺點:
1.無法取消Promise,一旦新建它就會立即執行,無法中途取消。
2.如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
3.當處於Pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
關於Promise的詳細介紹和用法,可以參考JavaScript Promise迷你書
2.為什麼要在js中使用Promise
ES6新增了Promise這個特性的意義在於,以往在js中處理非同步操作通常是使用回撥函式和事件,而有了Promise物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。拿node.js讀取檔案舉例子,基於JavaScript的非同步處理,以往都是想下面這樣利用回撥函式:
var fs = require('fs');
fs.readFile('demo.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data);
});
複製程式碼
而使用Promise可以這樣寫:
var fs = require('fs');
function readFile(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
readFile(('demo.txt').then(
function(data) {
console.log(data);
},
function(err) {
throw err;
}
);
複製程式碼
這樣的結構就比較清晰了,有同學看到這要問了,要是有多重巢狀怎麼辦,來看下面這個例子,假如我們有多個延時任務要處理,在js中便使用setTimeout來實現,在以往就是js中往往是這樣寫:
var taskFun = function() {
setTimeout(function() {
// do timeoutTask1
console.log("do timeoutTask1");
setTimeout(function() {
// do timeoutTask2
console.log("do timeoutTask2");
setTimeout(function() {
// dotimeoutTask3
console.log("do timeoutTask3");
}, 3000);
}, 1000);
}, 2000);
}
taskFun();
複製程式碼
這樣寫巢狀了多層回撥結構,如果業務邏輯再複雜一點,就會進入到所謂的回撥地獄,那麼如果用Promise可以這樣來寫:
new Promise(function(resolve, reject) {
console.log("start timeoutTask1");
setTimeout(resolve, 3000);
}).then(function() {
// do timeoutTask1
console.log("do timeoutTask1");
return new Promise(function(resolve, reject) {
console.log("start timeoutTask2");
setTimeout(resolve, 1000);
});
}).then(function() {
// do timeoutTask1
console.log("do timeoutTask2");
return new Promise(function(resolve, reject) {
console.log("start timeoutTask3");
setTimeout(resolve, 2000);
});
}).then(function() {
// do timeoutTask1
console.log("do timeoutTask3");
});
複製程式碼
我們還可以用Promise這樣寫,把每個任務提煉成單獨函式,讓程式碼看起來更加優雅直觀:
function timeoutTask1() {
return new Promise(function(resolve, reject) {
console.log("start timeoutTask1");
setTimeout(resolve, 3000);
});
}
function timeoutTask2() {
return new Promise(function(resolve, reject) {
console.log("start timeoutTask2");
setTimeout(resolve, 1000);
});
}
function timeoutTask3() {
return new Promise(function(resolve, reject) {
console.log("start timeoutTask3");
setTimeout(resolve, 2000);
});
}
timeoutTask1()
.then(function() {
// do timeoutTask1
console.log("do timeoutTask1");
})
.then(timeoutTask2)
.then(function() {
// do timeoutTask2
console.log("do timeoutTask2");
})
.then(timeoutTask3)
.then(function() {
// do timeoutTask2
console.log("do timeoutTask3");
});
複製程式碼
執行的順序為:
二.用ES6自己實現一個遵循Promise/A+規範的Promise
Promise/A+是Promise的一個主流規範,瀏覽器,node和JS庫依據此規範來實現相應的功能,以此規範來實現一個Promise也可以叫做實現一個Promise/A+。具體內容可參考Promise/A+規範
1.類和構造器的構建
Promise 的引數是一個函式 task,把內部定義 resolve 和reject方法作為引數傳到 task中,呼叫 task。當非同步操作成功後會呼叫 resolve 方法,然後就會執行 then 中註冊的回撥函式,失敗是呼叫reject方法。
class Promise {
constructor(task) {
let self = this; //快取this
self.status = 'pending'; //預設狀態為pending
self.value = undefined; //存放著此promise的結果
self.onResolvedCallbacks = []; //存放著所有成功的回撥函式
self.onRejectedCallbacks = []; //存放著所有的失敗的回撥函式
// 呼叫resolve方法可以把promise狀態變成成功態
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(() => { // 非同步執行所有的回撥函式
// 如果當前狀態是初始態(pending),則轉成成功態
// 此處這個寫判斷的原因是因為resolved和rejected兩個狀態只能由pending轉化而來,兩者不能相互轉化
if (self.status == 'pending') {
self.value = value;
self.status = 'resolved';
self.onResolvedCallbacks.forEach(item => item(self.value));
}
});
}
// 呼叫reject方法可以把當前的promise狀態變成失敗態
function reject(value) {
setTimeout(() => {
if (self.status == 'pending') {
self.value = value;
self.status = 'rejected';
self.onRejectedCallbacks.forEach(item => item(value));
}
});
}
// 立即執行傳入的任務
try {
task(resolve, reject);
} catch (e) {
reject(e);
}
}
}
複製程式碼
程式碼思路與要點:
- self = this, 不用擔心this指向突然改變問題。
- 每個 Promise 存在三個互斥狀態:pending、fulfilled、rejected。
- Promise 物件的狀態改變,只有兩種可能:從 pending 變為 fulfilled 和從 pending 變為 rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果。就算改變已經發生了,你再對 Promise 物件新增回撥函式,也會立即得到這個結果。這與事件完全不同,事件的特點是,如果你錯過了它,再去監聽,是得不到結果的。
- 建立 Promise 物件同時,呼叫其 task, 並傳入 resolve和reject 方法,當 task 的非同步操作執行成功後,就會呼叫 resolve,也就是執行 Promise .onResolvedCallbacks 陣列中的回撥,執行失敗時同理。
- resolve和reject 方法 接收一個引數value,即非同步操作返回的結果,方便傳值。
2.Promise.prototype.then鏈式支援
/**
* onFulfilled成功的回撥,onReject失敗的回撥
* 原型鏈方法
*/
then(onFulfilled, onRejected) {
let self = this;
// 當呼叫時沒有寫函式給它一個預設函式值
onFulfilled = isFunction(onFulfilled) ? onFulfilled : value => value;
onRejected = isFunction(onRejected) ? onRejected : value => {
throw value
};
let promise2;
if (self.status == 'resolved') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
if (self.status == 'rejected') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(self.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
if (self.status == 'pending') {
promise2 = new Promise((resolve, reject) => {
self.onResolvedCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
self.onRejectedCallbacks.push(value => {
try {
let x = onRejected(value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
});
}
return promise2;
}
複製程式碼
程式碼思路與要點:
- 呼叫 then 方法,將成功回撥放入 promise.onResolvedCallbacks 陣列;失敗回撥放入 promise.onRejectedCallbacks 陣列
- 返回一個 Promise 例項 promise2,方便鏈式呼叫
- then方法中的 return promise2 實現了鏈式呼叫
- 如果傳入的是一個不包含非同步操作的函式,resolve就會先於 then 執行,即 promise.onResolvedCallbacks 是一個空陣列,為了解決這個問題,在 resolve 函式中新增 setTimeout,將 resolve 中執行回撥的邏輯放置到 JS 任務佇列末尾;reject函式同理。
3.靜態方法Promise.resolve
static resolve(value) {
return new Promise((resolve, reject) => {
if (typeof value !== null && typeof value === 'object' && isFunction(value.then)) {
value.then();
} else {
resolve(value);
}
})
}
複製程式碼
靜態方法Promise.resolve(value)
可以認為是 new Promise()
方法的快捷方式。
比如 Promise.resolve(666);
可以認為是以下程式碼的語法糖。
new Promise(function(resolve){
resolve(666);
});
複製程式碼
4.靜態方法Promise.reject
static reject(err) {
return new Promise((resolve, reject) => {
reject(err);
})
}
複製程式碼
Promise.reject(err)
是和 Promise.resolve(value)
類似的靜態方法,是 new Promise()
方法的快捷方式。
比如 Promise.reject(new Error("出錯了"))
就是下面程式碼的語法糖形式。
new Promise(function(resolve,reject){
reject(new Error("出錯了"));
});
複製程式碼
4.靜態方法Promise.all
/**
* all方法,可以傳入多個promise,全部執行完後會將結果以陣列的方式返回,如果有一個失敗就返回失敗
* 靜態方法為類自己的方法,不在原型鏈上
*/
static all(promises) {
return new Promise((resolve, reject) => {
let result = []; // all方法最終返回的結果
let count = 0; // 完成的數量
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, err => {
reject(err);
});
}
});
}
複製程式碼
Promise.all
接收一個 promise物件的陣列作為引數,當這個陣列裡的所有promise物件全部變為resolve或reject狀態的時候,它才會去呼叫.then
方法。當全部為resolve時返回一個全部的resolve執行結果陣列,只要有一個不為resolve狀態,直接返回這個狀態的執行失敗結果。
5.靜態方法Promise.race
/**
* race方法,可以傳入多個promise,返回的是第一個執行完的resolve的結果,如果有一個失敗就返回失敗
* 靜態方法為類自己的方法,不在原型鏈上
*/
static race(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
resolve(data);
},err => {
reject(err);
});
}
});
}
複製程式碼
Promise.race
和Promise.all
相類似,它同樣接收一個陣列,race的意思是競賽,顧名思義只要是競賽就有唯一的那個第一名,所以它與all最大的不同是隻要該陣列中的任意一個 Promise 物件的狀態發生變化(無論是 resolve 還是 reject)該方法都會返回,所以它只輸出某一個最先執行的狀態結果,而不是像all一樣在全部為resolve狀態時返回的是一個陣列。只需在Promise.all 方法基礎上修改一下就可實現race。
三.總結
原始碼 以上是對幾個主要方法的介紹,還有些沒有介紹完全,可以參考原始碼,原始碼檔案裡包含了一個測試資料夾以及es5的版本原始碼,後續會奉上更為詳盡的解釋。另外可以通過安裝一個外掛來對實現的promise進行規範測試。
npm(cnpm) i -g promises-aplus-tests
promises-aplus-tests es6Promise.js
複製程式碼