一.起源
JavaScript中的異步由來已久,不論是定時函數,事件處理函數還是ajax異步加載都是異步編程的一種形式,我們現在以nodejs中異步讀取文件為例來編寫一個傳統意義的異步函數:
var fs = require('fs');
function readJSON(filename,callback){
fs.readFile(filename,'utf8',function(err,res){
if(err){
return callback(err,null);
}
try{
var res = JSON.parse(res);
}catch(ex){
callback(ex)
}
callback(null,res);
});
}
如果我們想異步讀取一個json文件,它接受2個參數,一個文件名,一個回調函數。文件名必不可少,關鍵就在這個callback上面了,當我們要執行這個readJSON函數時,要自己構造想要的回調函數,但是在readJSON函數內部傳遞callback時候不知道他的參數,顯然是不友好的。下面在看一種異步編程的代碼:
fs.readFile('file1.txt','utf8',function(err,res){
fs.readFile('file2.txt','utf8',function(err,res){
fs.readFile('file2.txt','utf8',function(err,res){
console.log(res);
});
});
});
這裡嵌套了3個異步回調函數,他們的執行時刻都是不可預測的並且這樣寫代碼也不符合普通程序的執行流程。
所以,問題來了,promise提供了一個解決上述問題的模式。
二.定義
promise是對異步編程的一種抽象。它是一個代理對象,代表一個必須進行異步處理的函數返回的值或拋出的異常。也就是說promise對象代表了一個異步操作,可以將異步對象和回調函數脫離開來,通過then方法在這個異步操作上面綁定回調函數。
下面介紹具體API,這裡遵循的是commonJS promise/A+規范。
1.狀態
promise有3種狀態:pending(待解決,這也是初始化狀態),fulfilled(完成),rejected(拒絕)。
2.接口
promise唯一接口then方法,它需要2個參數,分別是resolveHandler和rejectedHandler。並且返回一個promise對象來支持鏈式調用。
promise的構造函數接受一個函數參數,參數形式是固定的異步任務,舉一個栗子:
function sendXHR(resolve, reject){
var xhr = new XMLHttpRequest();
xhr.open('get', 'QueryUser', true);
xhr.onload = function(){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304){
resolve(xhr.responseText);
}else{
reject(new Error(xhr.statusText));
}
};
xhr.onerror = function(){
reject(new Error(xhr.statusText));
}
xhr.send(null)
}
三.實現
要實現promise對象,首先要考慮幾個問題:
1.promise構造函數中要實現異步對象狀態和回調函數的剝離,並且分離之後能夠還能使回調函數正常執行
2.如何實現鏈式調用並且管理狀態
首先是構造函數:
//全局宏定義
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;
//Promise構造函數
function Promise(fn){
var self = this;
self.state = PENDING;//初始化狀態
self.value = null;//存儲異步結果的對象變量
self.handlers = [];//存儲回調函數,這裡沒保存失敗回調函數,因為這是一個dome
//異步任務成功後處理,這不是回調函數
function fulfill(result){
if(self.state === PENDING){
self.state = FULFILLED;
self.value = result;
for(var i=0;i<self.handlers.length;i++){
self.handlers[i](result);
}
}
}
//異步任務失敗後的處理,
function reject(err){
if(self.state === PENDING){
self.state = REJECTED;
self.value = err;
}
}
fn&&fn(fulfill,reject);
};
構造函數接受一個異步函數,並且執行這個異步函數,修改promise對象的狀態和結果。
回調函數方法then:
//使用then方法添加回調函數,把這次回調函數return的結果當做return的promise的resolve的參數
Promise.prototype.then = function(onResolved, onRejected){
var self = this;
return new Promise(function(resolve, reject){
var onResolvedFade = function(val){
var ret = onResolved?onResolved(val):val;//這一步主要是then方法中傳入的成功回調函數通過return來進行鏈式傳遞結果參數
if(Promise.isPromise(ret)){//回調函數返回值也是promise的時候
ret.then(function(val){
resolve(val);
});
}
else{
resolve(ret);
}
};
var onRejectedFade = function(val){
var ret = onRejected?onRejected(val):val;
reject(ret);
};
self.handlers.push(onResolvedFade);
if(self._status === FULFILLED){
onResolvedFade(self._value);
}
if(self._status === REJECTED){
onRejectedFade(self._value);
}
});
}
通過上面的代碼可以看出,前面提出的2個問題得到了解決,1.在promise對象中有3個屬性,state,value,handlers,這3個屬性解決了狀態和回調的脫離,並且在調用then方法的時候才將回調函數push到handlers屬性上面(此時state就是1,可以在後面的代碼中執行onResolve)2.鏈式調用通過在then方法中返回的promise對象實現,並且通過onResolvedFade將上一個回調的返回值當做這次的result參數來執行進行傳遞。
測試代碼:
function async(value){
var pms = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(value);;
}, 1000);
});
return pms;
}
async(1).then(function(result){
console.log('the result is ',result);//the result is 2
return result;
}).then(function(result){
console.log(++result);//2
});
總之,不同框架對promise的實現大同小異,上面的代碼是ECMASCRIPT6標准的promise簡單實現。jquery和其他框架也有實現,不過聽說jquery的實現很糟糕- -!