Promise 物件代表了未來將要發生的事件,用來傳遞非同步操作的訊息。ECMAscript 6 原生提供了 Promise 物件。而且ES7中的async/await也是Promise基礎實現的。Promise到底有魅力和作用呢?本文將解開它的面紗,探索promise的原理及用法。不用再被一些回撥地獄、各種非同步呼叫所頭疼煩惱。
什麼是Promise?
promise,是承諾的意思。在JavaScript中promise指一個的物件或函式(是一個包含了相容promise規範then方法的物件或函式)。 Promise 物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise 物件提供統一的介面,使得控制非同步操作更加容易。
- promise的特點
1-1、 Promise的三種狀態
- pending: Promise物件例項建立時候的初始狀態
- resolved:可以理解為成功的狀態
- rejected:可以理解為失敗的狀態
- 如果是pending狀態,則promise:可以轉換到resolved或rejected狀態。
- 如果是resolved狀態,則promise:不能轉換成任何其它狀態。必須有一個值,且這個值不能被改變。
- 如果是rejected狀態,則promise可以:不能轉換成任何其它狀態。必須有一個原因,且這個值不能被改變。
”值不能被改變”指的是其identity不能被改變,而不是指其成員內容不能被改變。
1-3、promise 有一個 then 方法,就是用來指定Promise 物件的狀態改變時確定執行的操作,resolve 時執行第一個函式 (onFulfilled),reject 時執行第二個函式(onRejected)
promiseFn().then(resolve(onFulfilled){
//當promise狀態變成fulfilled時,呼叫此函式
},reject(onRejected){
//當promise狀態變成rejected時,呼叫此函式
});複製程式碼
- resolve,reject 都是可選引數
- 如果
onFulfilled
不是一個函式,則忽略之。 - 如果
onRejected
不是一個函式,則忽略之。
- 如果
onFulfilled
是一個函式:- 它必須在
promise
fulfilled後呼叫, 且promise
的value為其第一個引數。 - 它不能在
promise
fulfilled前呼叫。 - 不能被多次呼叫。
- 它必須在
- 如果
onRejected
是一個函式,- 它必須在
promise
rejected後呼叫, 且promise
的reason為其第一個引數。 - 它不能在
promise
rejected前呼叫。 - 不能被多次呼叫。
- 它必須在
onFulfilled
和onRejected
只允許在 execution context 棧僅包含平臺程式碼時執行.onFulfilled
和onRejected
必須被當做函式呼叫 (i.e. 即函式體內的this
為undefined
).- 對於一個
promise
,它的then方法可以呼叫多次.- 當
promise
fulfilled後,所有onFulfilled
都必須按照其註冊順序執行。 - 當
promise
rejected後,所有OnRejected
都必須按照其註冊順序執行。
- 當
then
必須返回一個promise .promise2 = promise1.then(onFulfilled, onRejected); 複製程式碼
- 如果
onFulfilled
或onRejected
返回了值x
, 則執行Promise 解析流程[[Resolve]](promise2, x)
. - 如果
onFulfilled
或onRejected
丟擲了異常e
, 則promise2
應當以e
為reason
被拒絕。 - 如果
onFulfilled
不是一個函式且promise1
已經fulfilled,則promise2
必須以promise1
的值fulfilled. - 如果
OnReject
不是一個函式且promise1
已經rejected, 則promise2
必須以相同的reason被拒絕.
- 如果
2、為什麼使用promise?有什麼好處呢?
- 2-1 對於回撥函式: 可以解決回撥地獄,如下圖: 例如,使用jQuery的ajax多次向後臺請求資料時,並且每個請求之間需要相互依賴,則需要回撥函式巢狀來解決而形成“回撥地獄”。
$.get(url1, data1 => { console.log(data1,"第一次請求"); $.get(data1.url, data2 => { // 第一次請求後的返回url 在此請求後臺 console.log(data2,"第二次請求") ..... }) }) 複製程式碼
這樣一來,在處理越多的非同步邏輯時,就需要越深的回撥巢狀,
複製程式碼
這種編碼模式的問題主要有以下幾個:
- 程式碼邏輯書寫順序與執行順序不一致,不利於閱讀與維護。
- 非同步操作的順序變更時,需要大規模的程式碼重構。
- 回撥函式基本都是匿名函式,bug 追蹤困難。
- 回撥函式是被第三方庫程式碼(如上例中的 ajax )而非自己的業務程式碼所呼叫的,造成了 IoC 控制反轉。
- 結果不能通過return返回
Promise 怎麼解決呢?
let p = url1 => {
return new Promise((resolve, reject) => {
$.get(url, data => {
resolve(data)
});
})
};
//
p(url).then(resvloe => {
return p(resvloe.url);
}).then(resvloe2 => {
return resvloe2(resvloe2.url);
}).then(resvloe3 => {
console.log(resvloe3);
}).catch(err => throw new Error(err));
當第一個then中返回一個promise,會將返回的promise的結果,傳遞到下一個then中。
這就是比較著名的鏈式呼叫了。 複製程式碼
- 2-2、Promise 也有一些缺點。
3、Promise 使用
- 1. 建立Promise。要想建立一個 promise 物件、可以使用 new 來呼叫 Promise 的構造器來進行例項化。接收一個excutor執行函式作為引數, excutor有兩個函式型別形參resolve reject。
let promise = new Promise(function(resolve, reject) {
// 非同步處理
// 處理結束後、呼叫resolve 或 reject
});
複製程式碼
- 2.promise中的狀態變化。
2. 當呼叫resolve(成功),會由pending => resolved
3. 當呼叫reject(失敗),會由pending => rejected
- 3. promise物件方法 then方法
// onFulfilled 是用來接收promise成功的值
// onRejected 是用來接收promise失敗的原因
// then方法是非同步的
promise.then(onFulfilled, onRejected);複製程式碼
3-2. resolve(成功): onFulfilled會被呼叫
let promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected 不會被呼叫
})
複製程式碼
3-3.reject(失敗) onRejected會被呼叫
let promise = new Promise((resolve, reject) => {
reject('rejected'); // 狀態由 pending => rejected
});
promise.then(result => { // onFulfilled 不會被呼叫
}, reason => { // onRejected
console.log(reason); // 'rejected'
})
複製程式碼
3-4.promise.catch 方法:捕捉錯誤 (catch 方法是 promise.then(null, rejection) 的別名,用於指定發生錯誤時的回撥函式。)複製程式碼
promise.catch(onRejected)
相當於
promise.then(null, onRrejected);
// 注意
// onRejected 不能捕獲當前onFulfilled中的異常
promise.then(onFulfilled, onRrejected);
// 可以寫成:
promise.then(onFulfilled)
.catch(onRrejected); 複製程式碼
3-5、promise chain 方法每次呼叫 都返回一個新的promise物件 所以可以鏈式寫法Promise的靜態方法
function step1() {
console.log("step1");
}
function step2() {
console.log("step2");
}
function onRejected(error) {
console.log("錯誤方法:", error);
}
var promise = Promise.resolve();
promise
.then(step1)
.then(step2)
.catch(onRejected) // 捕獲前面then方法中的異常複製程式碼
4、Promise的方法是使用
- Promise.resolve 返回一個fulfilled狀態的promise物件
Promise.resolve('成功');
// 相當於
let promise = new Promise(resolve => {
resolve('成功');
});
複製程式碼
//庫中實現
Promise.resolve = function (val) { return new Promise((resolve, reject) => resolve(val))}
複製程式碼
2.Promise.reject 返回一個rejected狀態的promise物件
var p = Promise.reject('出錯了');
p.then(null, function (s){
console.log(s)
});
// 出錯了
複製程式碼
3.Promise.all 接收一個promise物件陣列為引數
只有全部為resolve才會呼叫 通常會用來處理 多個並行非同步操作
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = new Promise((resolve, reject) => {
resolve(2);
});
const p3 = new Promise((resolve, reject) => {
reject(3);
});
Promise.all([p1, p2, p3]).then(data => {
console.log(data); // [1, 2, 3] 結果順序和promise例項陣列順序是一致的
}, err => {
console.log(err);
});
複製程式碼
//庫中實現
Promise.all = function(arr) {
return new Promise((resolve, reject) => {
let num = 0,innerArr = [];
function done(index,data){
innerArr[index] = data;
num ++;
if(num === arr.length){
resolve(innerArr);
} }
for(let i =0 ;i<arr.length;i++){
arr[i].then((res)=>{
done(i,res);
},reject); // 有一個失敗 就返回
}
})}
複製程式碼
4.Promise.race 接收一個promise物件陣列為引數
Promise.race 只要有一個promise物件進入 FulFilled 或者 Rejected 狀態的話,就會繼續進行後面的處理。
function timerPromisefy(delay) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(delay);
}, delay);
});
}
var startDate = Date.now();
Promise.race([
timerPromisefy(10),
timerPromisefy(20),
timerPromisefy(30)
]).then(function (values) {
console.log(values); // 10
});
複製程式碼
// 庫中實現
Promise.race = function(arr) {
return new Promise((resolve, reject) => {
arr.forEach((item, index) => {
item.then(resolve, reject);
});
});}
複製程式碼
5.Promise的finally
Promise.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};複製程式碼
5、prmoise 程式碼庫實現
/** * Promise 實現 遵循promise/A+規範 * Promise/A+規範譯文: * https://promisesaplus.com/ */
// 判斷x是不是promise 根據規範
function resolvePromise(promise2, x, resolve, reject) {
// 1、如果promise2 和 x 指向相同的值, 使用 TypeError做為原因將promise拒絕。 (就會導致迴圈引用報錯)
if (promise2 === x) {
return reject(new TypeError('迴圈引用'));
}
// 避免多次呼叫
let isUsed = false;
// 2、如果x是一個promise物件 (該判斷和下面 判斷是不是thenable(thenable 是一個包含了then方法的物件或函式)物件重複 所以可有可無)
/** * 如果x是pending狀態,promise必須保持pending走到x resolved或rejected.
如果x是resolved狀態,將x的值用於resolve promise. 如果x是rejected狀態, 將x的原因用於reject promise..
*/
// if (x instanceof Promise) { // 獲得它的終值 繼續resolve
// if (x.status === 'pending') { // 如果為等待態需等待直至 x 被執行或拒絕 並解析y值 遞迴
// x.then(y => {
// resolvePromise(promise2, y, resolve, reject);
// }, reason => {
// reject(reason);
// });
// } else {
// 如果 x 已經處於執行態/拒絕態(值已經被解析為普通值),用相同的值執行傳遞下去 promise
// x.then(resolve, reject);
// }
//
// 3、如果 x 為物件或者函式
//
/**
// * 1.將 then 賦為 x.then.
// 2.如果在取x.then值時丟擲了異常,則以這個異常做為原因將promise拒絕。
// 3.如果 then 是一個函式, 以x為this呼叫then函式, 且第一個引數是resolve,第二個引數是reject,且:
// 3-1.當 resolve 被以 y為引數呼叫, 執行 [[Resolve]](promise, y).
// 3-2.當 reject 被以 reason 為引數呼叫, 則以reason為原因將promise拒絕。
// 3-3.如果 resolvee 和 reject 都被呼叫了,或者被呼叫了多次,則只第一次有效,後面的忽略。
// 3-4.如果在呼叫then時丟擲了異常,則:
// 如果 resolve 或 reject 已經被呼叫了,則忽略它。isUsed = true;
// 否則, 以e為reason將 promise 拒絕。
// 4.如果 then不是一個函式,則 以x為值resolve promise。
//
*/
// } else
if (x !== null && ((typeof x === 'object') || (typeof x === 'function'))) {
try { // 是否是thenable物件(具有then方法的物件/函式) 如果在取x.then值時丟擲了異常,則以這個異常做為原因將promise拒絕。
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => { // 如果y是promise就繼續遞迴解析promise
if (isUsed) return;
isUsed = true;
resolvePromise(promise2, y, resolve, reject);
}, reason => { // 只要失敗了就失敗了 不用再遞迴解析是都是promise
if (isUsed) return;
isUsed = true;
reject(reason);
})
} else { // 說明是一個函式,則 以x為值resolve promise。
resolve(x);
}
} catch (e) {
if (isUsed) return;
isUsed = true;
reject(e);
}
} else { //4、如果 x 不是物件也不是函式,則以x為值 resolve promise。例如 x = 123 或 x ='成功'
resolve(x);
}}
// Promise 物件代表了未來將要發生的事件,用來傳遞非同步操作的訊息。
// Promise 物件,就可以將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise 物件提供統一的介面,使得控制非同步操作更加容易。
class Promise { // Promise 是一個類, new Promise 返回一個 promise物件 接收一個ex執行函式作為引數, ex有兩個函式型別形參resolve reject
/**
* var promise = new Promise(function(resolve, reject) { //會立即執行 // 非同步處理
// 處理結束後、呼叫resolve 或 reject
//當非同步程式碼執行成功時,我們才會呼叫resolve(...), 當非同步程式碼失敗時就會呼叫reject(...)
//在本例中,我們使用setTimeout(...)來模擬非同步程式碼,實際編碼時可能是XHR請求或是HTML5的一些API方法.
setTimeout(function(){
resolve("成功!"); //程式碼正常執行!
}, 250);
});
promise.then(function(successMessage){
//successMessage的值是上面呼叫resolve(...)方法傳入的值.
//successMessage引數不一定非要是字串型別,這裡只是舉個例子
console.log("Yay! " + successMessage);
});
*/
constructor(ex) { //
this.status = 'pending'; // 初始狀態 (表示 未開始)
this.resolveVal = undefined; // resolved狀態時(表示成功) 返回的資訊
this.rejectVal = undefined; // rejected狀態時(表示失敗) 返回的資訊
this.onResolveCallBackFns = []; // 儲存resolved狀態對應的onResolved函式 (因為可以鏈式呼叫可以多個then方法)
this.onRejectCallBackFns = []; // 儲存rejected狀態對應的onRejected函式
/**
* 一個Promise必須處在其中之一的狀態:pending, fulfilled 或 rejected.
* * 如果是pending狀態,則promise:
可以轉換到resolved或rejected狀態。
如果是resolved狀態,則promise:
不能轉換成任何其它狀態。
必須有一個值,且這個值不能被改變。
如果是rejected狀態,則promise可以:
不能轉換成任何其它狀態。
必須有一個原因,且這個值不能被改變。
”值不能被改變”指的是其identity不能被改變,而不是指其成員內容不能被改變。
*/
let resolve = (data) => { // data 成功態時接收的終值
if (this.status === 'pending') {
// 只能由pedning狀態 => resolved狀態 避免呼叫多次resolve reject)
this.status = 'resolved';
this.resolveVal = data;
this.onResolveCallBackFns.forEach(cb => cb());
}
}
let reject = (err) => {
if (this.status === 'pending') {
// 只能由pedning狀態 => rejected狀態 避免呼叫多次resolve reject)
this.status = 'rejected';
this.rejectVal = err;
this.onRejectCallBackFns.forEach(cb => cb());
}
}
// 捕獲在ex執行器中丟擲的異常
// new Promise((resolve, reject) => {
// throw new Error('error in ex')
// })
try {
ex(resolve, reject);
} catch (e) {
reject(e);
}
}
// 按照prmoise a+ 規範 then方法接受兩個引數 then方法是非同步執行的 必須返回一個promise then(resolve, reject) { // then 方法 resolve,reject 都是可選引數 保證引數後續能夠繼續執行 //
1.1、 如果resolve不是一個函式,則忽略之。 如果reject不是一個函式,則忽略之。
resolve = typeof resolve == 'function' ? resolve : y => y;
reject = typeof reject == 'function' ? reject : err => { throw err };
let promise2; // then必須返回一個promise
if (this.status === 'pending') { // 等待態
// 當非同步呼叫resolve/rejected時 將resolve/reject收集暫存到集合中
promise2 = new Promise((res, rej) => {
this.onResolveCallBackFns.push(() => {
setTimeout(() => {
try {
// resolvePromise可以解析x和promise2之間的關係 /** 如果resolve 或 reject 返回了值x, 則執行Promise 解析流程[[Resolve]](promise2, x). // 如果resolve 或 reject 丟擲了異常e, 則promise2應當以e為rejectVal被拒絕。 // 如果 resolve 不是一個函式且promise1已經resolved,則promise2必須以promise1的值resolved. // 如果 reject 不是一個函式且promise1已經rejected, 則promise2必須以相同的rejectVal被拒絕. */ let x = resolve(this.resolveVal); resolvePromise(promise2, x, res, rej); } catch (e) { rej(e); } }, 0); });
this.onRejectCallBackFns.push(() => {
setTimeout(() => {
try {
let x = reject(this.rejectVal);
resolvePromise(promise2, x, res, rej);
} catch (e) {
rej(e);
}
}, 0);
});
});
}
if (this.status == 'resolved') {
// 它必須在promise resolved後呼叫, 且promise的value為其第一個引數。
promise2 = new Promise((res, rej) => {
// 用setTimeout方法原因 :
// 1、方法是非同步的
// 2、 對於一個promise,它的then方法可以呼叫多次.(當在其他程式中多次呼叫同一個promise的then時 由於之前狀態已經為resolved/rejected狀態,則會走的下面邏輯),
// 所以要確保為resolved/rejected狀態後 也要非同步執行resolve/reject 保持統一
setTimeout(() => {
try {
let x = resolve(this.resolveVal);
resolvePromise(promise2, x, res, rej); //
// resolvePromise可以解析x和promise2之間的關係
} catch (e) {
rej(e);
}
})
})
}
if (this.status == 'rejected') { // 必須在promise rejected後呼叫, 且promise的rejectVal為其第一個引數
promise2 = new Promise((res, rej) => { // 方法是非同步的 所以用setTimeout方法
setTimeout(() => {
try {
let x = reject(this.rejectVal);
resolvePromise(promise2, x, res, rej); // resolvePromise可以解析x和promise2之間的關係
} catch (e) {
rej(e);
}
});
})
}
return promise2; // 呼叫then後返回一個新的promise }
// catch接收的引數 只用錯誤 catch就是then的沒有成功的簡寫
catch(err) {
return this.then(null, err);
}}
/** * Promise.all Promise進行並行處理
* 引數: arr物件組成的陣列作為引數
* 返回值: 返回一個Promise例項
* 當這個陣列裡的所有promise物件全部變為resolve狀態的時候,才會resolve。
*/
Promise.all = function (arr) {
return new Promise((resolve, reject) => {
let num = 0, innerArr = [];
function done(index, data) {
innerArr[index] = data;
num++;
if (num === arr.length) {
resolve(innerArr);
}
}
for (let i = 0; i < arr.length; i++) {
arr[i].then((res) => {
done(i, res);
}, reject); // 有一個失敗 就返回
} })}
/** * Promise.race
* 引數: 接收 promise物件組成的陣列作為引數
* 返回值: 返回一個Promise例項
* 只要有一個promise物件進入 resolved 或者 rejected 狀態的話,就會繼續進行後面的處理(取決於哪一個更快)
*/
Promise.race = function (arr) {
return new Promise((resolve, reject) => {
arr.forEach((item, index) => {
item.then(resolve, reject);
});
});}
// Promise.reject 返回一個rejected狀態的promise物件
Promise.resolve = function (val) {
return new Promise((resolve, reject) => resolve(val))}
// .Promise.resolve 返回一個fulfilled狀態的promise物件
Promise.reject = function (val) {
return new Promise((resolve, reject) => reject(val));}
Promise.deferred = Promise.defer = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;}
module.exports = Promise;複製程式碼
6、測試
let p = new Promise((resolve, reject) => {
reject('err');})
p.then().then().catch(r => {
console.log(r);}).
then(data => {
console.log('data', data);})
執行結果:
errdata undefined
複製程式碼
let fs = require('fs');
function read() {
// 好處就是解決巢狀問題
// 壞處錯誤處理不方便了
let defer = Promise.defer();
fs.readFile('./1.txt', 'utf8', (err, data) => {
if (err) defer.reject(err);
defer.resolve(data) });
return defer.promise;}
read().then(data => {
console.log(data);});
執行結果
我是1.txt內容複製程式碼