最近一直私下在看Android專案,前端這一塊沒怎麼仔細研究。昨天在寫重構公司前端專案的時候,我發現一旦有非同步的任務,腦海裡麵條件反射一般的出現promise的字樣。重構的多了 心就就在納悶:既然promise這麼好用,我能不能自己手寫一個promise呢?我思索了半天,按照自己的想法模擬了出來,但是和一位大佬交流的時候,他說我的寫法並沒有遵循Promise/A+規範。當時我心裡是懵逼的,加班的時候就一直想寫這個問題。
一、 promise是什麼?
1.1、 為什麼要用promise?
我們都知道,在javascript裡面所有的程式碼都是單執行緒執行的。由於這種“單執行緒”的弊端,所有javascript重的網路請求、瀏覽器事件等都只能用非同步來執行。傳統的非同步實現的方式都是用回撥函式來實現的,例如:
request.onreadystatechange = function () {
if (request.readyState === 4) {
if (request.status === RESULT_OK) {
return success(req.responseText);
}
return fail(req.status);
}
}
複製程式碼
這樣雖然比較直觀,但是並不符合程式設計師的審美習慣,程式碼的複用性也很低。按照現在主流的程式碼風格來說,一份優秀的程式碼模版就應該是連結串列形式的。
// Android寫習慣了,一般封裝網路都是okhttp 。不用理我 = =
okhttp.request()
.onSuccesul()
.onFail();
複製程式碼
於是promise應運而生。
1.2、 promise介紹
promise的中文釋意為:承諾。我覺得這個翻譯很能凸顯這個物件的含義。
promise裡面有三個狀態:
- pending:進行中
- fulfilled: 已成功
- rejected:已結束
為什麼說,承諾這個解釋很能凸顯promise物件的特性呢?我們來為您談談promise裡面的兩個特點:
1、 物件不能被任何手段來更改。怎麼解釋呢?只要是你下了承諾,就不能受任何外界環境的干擾,只受到一步操作的結果來決定。 2、 一旦狀態改變,就不會再變,並且任何時候都可以得到這個結果。
從上面的特性裡面,是不是更加加深了對承諾的理解呢!
二、 promise簡單探究。
前面說了這麼多,都是傻把式,下面我們就用程式碼體驗一把promise的快感吧 。
new Promise((resolve,reject)=>{
let randomNumber = Math.random()
console.log(randomNumber)
if(randomNumber>0.5){
resolve('success!!!')
}else{
reject('fail!!!')
}
}).then(res=>{
console.log(res);
},error=>{
console.log(error);
})
複製程式碼
從上面的程式碼我們可以比較清楚的發現,Promise接收兩個引數:resolve和reject。resolve函式的作用是,將Promise物件的狀態從“進行中”變為“成功”( pending => resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject函式的作用是,將Promise物件的狀態從“進行中”變為“失敗”( pending=>rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
並且在Promise物件例項生成後,可以用then來分別指定完成後的resolve和reject。並且then裡面的reject是可選項。
對於promise,我覺得主要有三個點需要給讀者講一下,來看下面一段程式碼:
let promise = new Promise((resolve,reject)=>{
let randomNumber = Math.random()
console.log(randomNumber)
if(randomNumber>0.5){
resolve('success!!!')
}else{
reject('fail!!!')
}
})
複製程式碼
我們發現單獨執行這一段程式碼的話,console依然被列印出來了。這說明promise是建立的時候就被執行的。 再來看一段程式碼:
let promise = new Promise((resolve,reject)=>{
let randomNumber = Math.random()
if(randomNumber>0.5){
resolve('success!!!')
}else{
reject('fail!!!')
}
console.log(randomNumber)
}).then(res=>{
console.log(res);
},error=>{
console.log(error)
})
複製程式碼
效果如下:
由上面的現象我們可以輕易的看出來:resolve和reject後的程式碼一般都還會執行,如果你想避免這種情況,你可以結合return來使用,例如:return resolve('success!!!');
return reject('fail!!!');
複製程式碼
三、 promise重要屬性探究
2.1、 then
Promise 例項具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的。它的作用是為 Promise 例項新增狀態改變時的回撥函式。then裡面有兩個回撥函式,前者返回resolve的回撥函式,後者是可選值。並且then返回返回一個新的promise,這樣就能一直使用鏈式結構了。
2.2、 catch
catch可以看成是then(null/undefined, reject)的別名 專門用來指定錯誤發生時的回撥函式。那麼為什麼要設計這個屬性呢?主要有兩個方面,首先來看下面一段程式碼:
// 普通then
.then(res=>{
console.log(res);
},error=>{
console.log(error)
})
//catch
.then(res=>{
console.log(res);
})
.catch(error=>{
console.log(error)
})
複製程式碼
明顯第二個相對前一個會更加優雅一點。
我們再來看一段程式碼:
// 普通then
.then(res=>{
throw Error("have exception")
console.log(res);
},error=>{
console.log(error)
})
// catch
.then(res=>{
throw Error("have exception")
}).catch(error=>{
console.log(error)
})
複製程式碼
執行上面的程式碼我們很清楚的發現catch能捕獲到then的異常,但是then的reject回撥裡面並不能捕獲到resolve的異常。這在一定程度上保證了程式碼的正常執行順序。
2.3、 finally
finally是es2018才引入的屬性,不管 Promise 物件最後狀態如何,都會執行的操作。其本質也是then方法的特性。
promise
.finally(() => {
// ...
});
// 等同於
promise
.then(
result => {
// ...
return result;
},
error => {
// ...
throw error;
}
);
複製程式碼
2.4、 all
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。其基礎的語法是:
const p = Promise.all([p1, p2, p3]);
複製程式碼
Promise.all方法接受一個陣列作為引數,p1、p2、p3都是 Promise 例項,如果不是,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。 關於p的狀態,主要由接收的promise陣列決定的 (1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。 (2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
這裡值得我們注意的一點是,如果p1,p2,p3中有錯誤 並且有自己的catch方法的時候。會呼叫其自己的catch,當沒有catch方法的時候才會拋給p的catch來處理
2.5、 race
Promise.race方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。其基礎的語法是:
const p = Promise.race([p1, p2, p3]);
複製程式碼
和all方法不同的是: (1)只有p1、p2、p3的狀態都變成rejected,p才會變成rejected (2) 如果p1、p2、p3中有一個狀態變成fulfilled,p的狀態就會變成fulfilled 並將首次變成fulfilled的返回值返回。
四、 初次手寫的promise
前面大概都將所謂的promise的用法給描述了一遍,大概寫一兩個例子就能感受出promise的奧祕。那麼如果讓你自己來寫一個promise,你應該如何來寫呢?
這是我下班後寫的一個Promise:
const PENDING = "pending";
const RESOLVE = "resolve";
const REJECTED = "rejected";
function JPromise(fn){
const that = this;
that.state = PENDING;
that.value = null;
that.resolvedCallbacks = [];
that.rejectedCallbacks = [];
function resolve(value){
if(that.state === PENDING){
that.state = RESOLVE;
that.value = value;
that.resolvedCallbacks.map(cb=>cb(that.value));
}
}
function reject(value){
if(that.state === PENDING){
that.state = REJECTED;
that.value = value;
that.rejectedCallbacks.map(cb=>cb(that.value))
}
}
try{
fn(resolve,reject)
}catch(e){
reject(e)
}
}
JPromise.prototype.then = function(onFulfilled,onRejected){
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:v=>v;
onRejected = typeof onRejected === 'function'?onRejected:r=>new Error(r);
if(this.state === PENDING){
this.resolvedCallbacks.push(onFulfilled)
this.rejectedCallbacks.push(onRejected)
}
if(this.state ===RESOLVE){
onFulfilled(this.value);
}
if(this.state === REJECTED){
onRejected(this.value)
}
}
複製程式碼
然後執行一下:
new JPromise((resolve,rejected)=>{
resolve(1)
}).then(res=>{
console.log(res);
},error=>{
console.log(error);
})
複製程式碼
然後自測了一下,完美能實現簡單的promise。但是跟大佬交流之後才知道:原來promise有自己的一套標準,我這套程式碼雖然能夠簡單的實現promise的功能 但是沒有達到那套標準。
五、 符合規範的promise
首先關於promise規範,我就簡單的提一下吧:
- 每個then方法都返回一個新的Promise物件(原理的核心)
- 如果then方法中顯示地返回了一個Promise物件就以此物件為準,返回它的結果
- 如果then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise物件返回。
- 如果then方法中沒有return語句,就視為返回一個用Undefined包裝的Promise物件
- 若then方法中出現異常,則呼叫失敗態方法(reject)跳轉到下一個then的onRejected
- 如果then方法沒有傳入任何回撥,則繼續向下傳遞(值的傳遞特性)。
最後張貼一下 我改進的程式碼:
const PENDING = 'pending';//初始態
const FULFILLED = 'fulfilled';//初始態
const REJECTED = 'rejected';//初始態
function Promise(executor){
let self = this;//先快取當前promise例項
self.status = PENDING;//設定狀態
//定義存放成功的回撥的陣列
self.onResolvedCallbacks = [];
//定義存放失敗回撥的陣列
self.onRejectedCallbacks = [];
//當呼叫此方法的時候,如果promise狀態為pending,的話可以轉成成功態,如果已經是成功態或者失敗態了,則什麼都不做
function resolve(value){ //2.1.1
if(value!=null &&value.then&&typeof value.then == 'function'){
return value.then(resolve,reject);
}
//如果是初始態,則轉成成功態
//為什麼要把它用setTimeout包起來
setTimeout(function(){
if(self.status == PENDING){
self.status = FULFILLED;
self.value = value;//成功後會得到一個值,這個值不能改
//呼叫所有成功的回撥
self.onResolvedCallbacks.forEach(cb=>cb(self.value));
}
})
}
function reject(reason){ //2.1.2
setTimeout(function(){
//如果是初始態,則轉成失敗態
if(self.status == PENDING){
self.status = REJECTED;
self.value = reason;//失敗的原因給了value
self.onRejectedCallbacks.forEach(cb=>cb(self.value));
}
});
}
try{
//因為此函式執行可能會異常,所以需要捕獲,如果出錯了,需要用錯誤 物件reject
executor(resolve,reject);
}catch(e){
//如果這函式執行失敗了,則用失敗的原因reject這個promise
reject(e);
};
}
function resolvePromise(promise2,x,resolve,reject){
if(promise2 === x){
return reject(new TypeError('迴圈引用'));
}
let called = false;//promise2是否已經resolve 或reject了
if(x instanceof Promise){
if(x.status == PENDING){
x.then(function(y){
resolvePromise(promise2,y,resolve,reject);
},reject);
}else{
x.then(resolve,reject);
}
//x是一個thenable物件或函式,只要有then方法的物件,
}else if(x!= null &&((typeof x=='object')||(typeof x == 'function'))){
//當我們的promise和別的promise進行互動,編寫這段程式碼的時候儘量的考慮相容性,允許別人瞎寫
try{
let then = x.then;
if(typeof then == 'function'){
//有些promise會同時執行成功和失敗的回撥
then.call(x,function(y){
//如果promise2已經成功或失敗了,則不會再處理了
if(called)return;
called = true;
resolvePromise(promise2,y,resolve,reject)
},function(err){
if(called)return;
called = true;
reject(err);
});
}else{
//到此的話x不是一個thenable物件,那直接把它當成值resolve promise2就可以了
resolve(x);
}
}catch(e){
if(called)return;
called = true;
reject(e);
}
}else{
//如果X是一個普通 的值,則用x的值去resolve promise2
resolve(x);
}
}
//onFulfilled 是用來接收promise成功的值或者失敗的原因
Promise.prototype.then = function(onFulfilled,onRejected){
//如果成功和失敗的回撥沒有傳,則表示這個then沒有任何邏輯,只會把值往後拋
//2.2.1
onFulfilled = typeof onFulfilled == 'function'?onFulfilled:function(value){return value};
onRejected = typeof onRejected == 'function'?onRejected:reason=>{throw reason};
//如果當前promise狀態已經是成功態了,onFulfilled直接取值
let self = this;
let promise2;
if(self.status == FULFILLED){
return promise2 = new Promise(function(resolve,reject){
setTimeout(function(){
try{
let x =onFulfilled(self.value);
//如果獲取到了返回值x,會走解析promise的過程
resolvePromise(promise2,x,resolve,reject);
}catch(e){
//如果執行成功的回撥過程中出錯了,用錯誤原因把promise2 reject
reject(e);
}
})
});
}
if(self.status == REJECTED){
return promise2 = new Promise(function(resolve,reject){
setTimeout(function(){
try{
let x =onRejected(self.value);
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
})
});
}
if(self.status == PENDING){
return promise2 = new Promise(function(resolve,reject){
self.onResolvedCallbacks.push(function(){
try{
let x =onFulfilled(self.value);
//如果獲取到了返回值x,會走解析promise的過程
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
});
self.onRejectedCallbacks.push(function(){
try{
let x =onRejected(self.value);
resolvePromise(promise2,x,resolve,reject);
}catch(e){
reject(e);
}
});
});
}
}
複製程式碼
具體的程式碼細節呢?我在程式碼中已經做了標註,如果又不懂的可以私聊、微信 歡迎來擾。
說在最後
已經有好幾個中午好怎麼睡午覺了,這篇文章寫的有點敷衍。本來下班的時候就8點半了,然後又整理/除錯程式碼 整了半天,結果在寫文章的時候想例子想個半天都想不出來。我明天會利用休息時間好好來修改一下這篇文章的,太困了 先這樣吧,我洗澡去睡覺了。您如果對現在的這篇文章不太滿意,就請過一天再來看。
最後提一句,能不能給我漲點人氣啊,寫了一年多,還是這點人氣..