原文在我的部落格中:原文地址 如果文章對您有幫助,您的star是對我最好的鼓勵~
簡要介紹:Promise允許我們通過鏈式呼叫的方式來解決“回撥地獄”的問題,特別是在非同步過程中,通過Promise可以保證程式碼的整潔性和可讀性。本文主要解讀Promise/A+規範,並在此規範的基礎上,自己實現一個Promise.
一、Promise的使用
在瞭解Promise規範之前,我們知道主流的高版本瀏覽器已經支援ECMA中的Promise.
建立一個promise例項:
var p=new Promise(function(resolve,reject){
setTimeout(function(){
resolve("success")
},1000);
console.log("建立一個新的promise");
})
p.then(function(x){
console.log(x)
})
//輸出:
建立一個新的promise
success
複製程式碼
上述是一個promise的例項,輸出內容為,“建立一個promise”,延遲1000ms後,輸出"success"。
從上述的例子可以看出,promise方便處理非同步操作。此外promise還可以鏈式的呼叫:
var p=new Promise(function(resolve,reject){resolve()});
p.then(...).then(...).then(...)
複製程式碼
此外Promise除了then方法外,還提供了Promise.resolve、Promise.all、Promise.race等等方法。
二、Promise/A+規範
Promise/A+規範擴充套件了早期的Promise/A proposal提案,我們來解讀一下Promise/A+規範。
1.術語
(1)"promise"是一個物件或者函式,該物件或者函式有一個then方法
(2)"thenable"是一個物件或者函式,用來定義then方法
(3)"value"是promise狀態成功時的值
(4)"reason"是promise狀態失敗時的值
我們明確術語的目的,是為了在自己實現promise時,保持程式碼的規範性(也可以跳過此小節)
2.要求
(1)一個promise必須有3個狀態,pending,fulfilled(resolved),rejected當處於pending狀態的時候,可以轉移到fulfilled(resolved)或者rejected狀態。當處於fulfilled(resolved)狀態或者rejected狀態的時候,就不可變。
promise英文譯為承諾,也就是說promise的狀態一旦發生改變,就永遠是不可逆的。
(2)一個promise必須有一個then方法,then方法接受兩個引數:
promise.then(onFulfilled,onRejected)
複製程式碼
其中onFulfilled方法表示狀態從pending——>fulfilled(resolved)時所執行的方法,而onRejected表示狀態從pending——>rejected所執行的方法。
(3)為了實現鏈式呼叫,then方法必須返回一個promise
promise2=promise1.then(onFulfilled,onRejected)
複製程式碼
三、實現一個符合Promise/A+規範的Promise
解讀了Promise/A+規範之後,下面我們來看如何實現一個Promise, 首先構造一個myPromise函式,關於所有變數和函式名,應該與規範中保持相同。
1.v1.0 初始版本myPromise
function myPromise(constructor){
let self=this;
self.status="pending" //定義狀態改變前的初始狀態
self.value=undefined;//定義狀態為resolved的時候的狀態
self.reason=undefined;//定義狀態為rejected的時候的狀態
function resolve(value){
//兩個==="pending",保證了狀態的改變是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//兩個==="pending",保證了狀態的改變是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕獲構造異常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
複製程式碼
同時,需要在myPromise的原型上定義鏈式呼叫的then方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
複製程式碼
上述就是一個初始版本的myPromise,在myPromise裡發生狀態改變,然後在相應的then方法裡面根據不同的狀態可以執行不同的操作。
var p=new myPromise(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//輸出1
複製程式碼
但是這裡myPromise無法處理非同步的resolve.比如:
var p=new myPromise(function(resolve,reject){setTimeout(function(){resolve(1)},1000)});
p.then(function(x){console.log(x)})
//無輸出
複製程式碼
2.v2.0基於觀察模式實現
為了處理非同步resolve,我們修改myPromise的定義,用2個陣列onFullfilledArray和onRejectedArray來儲存非同步的方法。在狀態發生改變時,一次遍歷執行陣列中的方法。
function myPromise(constructor){
let self=this;
self.status="pending" //定義狀態改變前的初始狀態
self.value=undefined;//定義狀態為resolved的時候的狀態
self.reason=undefined;//定義狀態為rejected的時候的狀態
self.onFullfilledArray=[];
self.onRejectedArray=[];
function resolve(value){
if(self.status==="pending"){
self.value=value;
self.status="resolved";
self.onFullfilledArray.forEach(function(f){
f(self.value);
//如果狀態從pending變為resolved,
//那麼就遍歷執行裡面的非同步方法
});
}
}
function reject(reason){
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
self.onRejectedArray.forEach(function(f){
f(self.reason);
//如果狀態從pending變為rejected,
//那麼就遍歷執行裡面的非同步方法
})
}
}
//捕獲構造異常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
複製程式碼
對於then方法,狀態為pending時,往陣列裡面新增方法:
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "pending":
self.onFullfilledArray.push(function(){
onFullfilled(self.value)
});
self.onRejectedArray.push(function(){
onRejected(self.reason)
});
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
複製程式碼
這樣,通過兩個陣列,在狀態發生改變之後再開始執行,這樣可以處理非同步resolve無法呼叫的問題。這個版本的myPromise就能處理所有的非同步,那麼這樣做就完整了嗎?
沒有,我們做Promise/A+規範的最大的特點就是鏈式呼叫,也就是說then方法返回的應該是一個promise。
3.v3.0then方法實現鏈式呼叫
要通過then方法實現鏈式呼叫,那麼也就是說then方法每次呼叫需要返回一個primise,同時在返回promise的構造體裡面,增加錯誤處理部分,我們來改造then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
let promise2;
switch(self.status){
case "pending":
promise2=new myPromise(function(resolve,reject){
self.onFullfilledArray.push(function(){
try{
let temple=onFullfilled(self.value);
resolve(temple)
}catch(e){
reject(e) //error catch
}
});
self.onRejectedArray.push(function(){
try{
let temple=onRejected(self.reason);
reject(temple)
}catch(e){
reject(e)// error catch
}
});
})
case "resolved":
promise2=new myPromise(function(resolve,reject){
try{
let temple=onFullfilled(self.value);
//將上次一then裡面的方法傳遞進下一個Promise的狀態
resolve(temple);
}catch(e){
reject(e);//error catch
}
})
break;
case "rejected":
promise2=new myPromise(function(resolve,reject){
try{
let temple=onRejected(self.reason);
//將then裡面的方法傳遞到下一個Promise的狀態裡
resolve(temple);
}catch(e){
reject(e);
}
})
break;
default:
}
return promise2;
}
複製程式碼
這樣通過then方法返回一個promise就可以實現鏈式的呼叫:
p.then(function(x){console.log(x)}).then(function(){console.log("鏈式呼叫1")}).then(function(){console.log("鏈式呼叫2")})
//輸出
1
鏈式呼叫1
鏈式呼叫2
複製程式碼
這樣我們雖然實現了then函式的鏈式呼叫,但是還有一個問題,就是在Promise/A+規範中then函式裡面的onFullfilled方法和onRejected方法的返回值可以是物件,函式,甚至是另一個promise。
4.v4.0 then函式中的onFullfilled和onRejected方法的返回值問題
特別的為了解決onFullfilled和onRejected方法的返回值可能是一個promise的問題。
(1)首先來看promise中對於onFullfilled函式的返回值的要求
I)如果onFullfilled函式返回的是該promise本身,那麼會丟擲型別錯誤
II)如果onFullfilled函式返回的是一個不同的promise,那麼執行該promise的then函式,在then函式裡將這個promise的狀態轉移給新的promise III)如果返回的是一個巢狀型別的promsie,那麼需要遞迴。
IV)如果返回的是非promsie的物件或者函式,那麼會選擇直接將該物件或者函式,給新的promise。
根據上述返回值的要求,我們要重新的定義resolve函式,這裡Promise/A+規範裡面稱為:resolvePromise函式,該函式接受當前的promise、onFullfilled函式或者onRejected函式的返回值、resolve和reject作為引數。
下面我們來看resolvePromise函式的定義:
function resolvePromise(promise,x,resolve,reject){
if(promise===x){
throw new TypeError("type error")
}
let isUsed;
if(x!==null&&(typeof x==="object"||typeof x==="function")){
try{
let then=x.then;
if(typeof then==="function"){
//是一個promise的情況
then.call(x,function(y){
if(isUsed)return;
isUsed=true;
resolvePromise(promise,y,resolve,reject);
},function(e){
if(isUsed)return;
isUsed=true;
reject(e);
})
}else{
//僅僅是一個函式或者是物件
resolve(x)
}
}catch(e){
if(isUsed)return;
isUsed=true;
reject(e);
}
}else{
//返回的基本型別,直接resolve
resolve(x)
}
}
複製程式碼
改變了resolvePromise函式之後,我們在then方法裡面的呼叫也變成了resolvePromise而不是promise。
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
let promise2;
switch(self.status){
case "pending":
promise2=new myPromise(function(resolve,reject){
self.onFullfilledArray.push(function(){
setTimeout(function(){
try{
let temple=onFullfilled(self.value);
resolvePromise(temple)
}catch(e){
reject(e) //error catch
}
})
});
self.onRejectedArray.push(function(){
setTimeout(function(){
try{
let temple=onRejected(self.reason);
resolvePromise(temple)
}catch(e){
reject(e)// error catch
}
})
});
})
case "resolved":
promise2=new myPromise(function(resolve,reject){
setTimeout(function(){
try{
let temple=onFullfilled(self.value);
//將上次一then裡面的方法傳遞進下一個Promise狀態
resolvePromise(temple);
}catch(e){
reject(e);//error catch
}
})
})
break;
case "rejected":
promise2=new myPromise(function(resolve,reject){
setTimeout(function(){
try{
let temple=onRejected(self.reason);
//將then裡面的方法傳遞到下一個Promise的狀態裡
resolvePromise(temple);
}catch(e){
reject(e);
}
})
})
break;
default:
}
return promise2;
}
複製程式碼
這樣就能處理onFullfilled各種返回值的情況。
var p=new Promise(function(resolve,reject){resolve("初始化promise")})
p.then(function(){return new Promise(function(resolve,reject){resolve("then裡面的promise返回值")})}).then(function(x){console.log(x)})
//輸出
then裡面promise的返回值
複製程式碼
到這裡可能有點亂,我們再理一理,首先返回值有兩個:
-
then函式的返回值——>返回一個新promise,從而實現鏈式呼叫
-
then函式中的onFullfilled和onRejected方法——>返回基本值或者新的promise
這兩者其實是有關聯的,onFullfilled方法的返回值可以決定then函式的返回值。
四、檢測是否完全符合Promise/A+規範
npm install -g promises-aplus-tests
複製程式碼
具體用法請看promise test然後
promises-aplus-tests myPromise.js
複製程式碼
結果為:
說明我們的實現完全符合Promise/A+規範。完整程式碼的地址
https://github.com/forthealllight/promise-achieve
五、最後補充Typescript實現的Promise/A+規範(可以忽略此節)
interface IConstructor{
(resolve:IResolve,reject:IReject):void
}
interface IResolve {
(x:any):void
}
interface IReject {
(x:any):void
}
function myPromise(constructor:IConstructor):void{
let self:object=this;
self.status="pending";
self.value=undefined;//if pending->resolved
self.reason=undefined;//if pending->rejected
self.onFullfilledArray=[];//to deal with async(resolved)
self.onRejectedArray=[];//to deal with async(rejeced)
let resolve:IResolve;
resolve=function(value:any):void{
//pending->resolved
if(self.status==="pending"){
self.status="resolved";
self.value=value;
self.onFullfilledArray.forEach(function(f){
f(self.value);
})
}
}
let reject:IReject;
reject=function(reason:any):void{
if(self.status==="pending"){
self.status="rejected";
self.reason=reason;
self.onRejectedArray.forEach(function(f){
f(self.reason);
})
}
}
//According to the definition that the function "constructor" accept two parameters
//error catch
try {
constructor(resolve,reject);
} catch (e) {
reject(e);
}
}
複製程式碼
單純的寫個工具函式,用ts還是有點影響可讀性。