背景
Promise是為了解決Javascript回撥巢狀過多導致回撥地獄(callbackhell)而產生的。目前已被納入了es2015規範,主流瀏覽器都支援Promise。為了在工作中更好的運用Promise,我們需要理解Promise規範與內部實現機制,下面我們來手動實現一個Promise。
Promise/A+規範
在寫程式碼之前讓我們先了解下 Promise/A+規範。
一個promise有三種狀態:
- pending:表示初始狀態,可以轉移到 fullfilled 或者 rejected 狀態
- fulfilled:表示操作成功,不可轉移狀態
- rejected:表示操作失敗,不可轉移狀態
- 必須有一個 then 非同步執行方法,then 接受兩個引數且必須返回一個promise:
借用這張來自MDN的流程圖我們可以清晰的看到 Promise 狀態的流轉過程。
簡單版
下面我們來實現一個簡單版的 Promise:
function Promise1(executor){
let self = this;
self.status = 'pending';
self.value = undefined;
self.reason = undefined;
function resolve(value) {
if(self.status==='pending'){
self.status = 'fullfilled';
self.value = value;
}
}
function reject(reason) {
if(self.status==='pending') {
self.status = 'rejected';
self.reason = reason;
}
}
try{
executor(resolve,reject);
}catch(e) {
reject(e);
}
}
Promise1.prototype.then = function(onFullfilled,onRejected) {
if(this.status==='fullfilled') {
onFullfilled(this.value);
}
if(this.status==='rejected') {
onRejected(this.reason);
}
}
//測試
let p= new Promise1(function(resolve,reject){resolve(1)});
p.then(function(x){console.log(x)})
//輸出1
複製程式碼
支援非同步
現在,我們實現了最簡單的 Promise。以上版本的Promise是存在很多問題的。為什麼呢?最大的問題是它不支援非同步,然而在現實中,Promise絕大多數使用場景都是非同步。讓我們來為 Promise 加入非同步功能。
const PENDING = 'pending';
const FULFILLED = 'fullfilled';
const REJECTED = 'rejected';
function Promise1(executor){
let self = this;
self.status = PENDING;
self.value = undefined;
self.reason = undefined;
self.fullfilledCallbacks = [];
self.rejectedCallbacks = [];
function resolve(value) {
if(value instanceof Promise) {
value.then(resolve,reject);
}
setTimeout(function(){
if(self.status===PENDING){
self.status = FULFILLED;
self.value = value;
self.fullfilledCallbacks.forEach(function(cb){
cb(self.value)
})
}
})
}
function reject(reason) {
setTimeout(function(){
if(self.status===PENDING) {
self.status = REJECTED;
self.reason = reason;
self.rejectedCallbacks.forEach(function(cb){
cb(self.reason);
})
}
})
}
try{
executor(resolve,reject);
}catch(e) {
reject(e);
}
}
Promise1.prototype.then = function(onFulfilled,onRejected) {
let self = this;
return new Promise1(function(resolve,reject){
function success(value) {
let _value = (typeof onFulfilled === 'function' && onFulfilled(value)) || value;
resolve(_value)
}
function error(reason) {
let _reason = (typeof onRejected === 'function' && onRejected(reason)) || reason;
reject(_reason);
}
if(self.status==PENDING) {
self.fullfilledCallbacks.push(success);
self.rejectedCallbacks.push(error);
} else if(self.status==FULLFILLED){
success.call(this,this.value)
} else if(self.status==REJECTED) {
error.call(this,this.reason);
}
})
}
複製程式碼
以上程式碼中,我們做了如下更改:
- 將 Promise 三個狀態定義為常量,方便維護
- 對於 Promise resolve和reject 函式執行加入非同步處理
- 在Promise.then中返回新的Promise物件,使Promise可以支援鏈式呼叫
錯誤處理以及靜態方法
下面讓我們來為Promise 新增錯誤處理以及靜態方法:
//錯誤處理
Promise1.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
//返回fullfilled Promise物件
Promise1.resolve = function (value) {
return new Promise1(resolve => {
resolve(value);
});
}
//返回 rejected Promise 物件
Promise1.reject = function (reason) {
return new Promise1((resolve, reject) => {
reject(reason);
});
}
//Promise.all方法
Promise1.all = function(promises) {
function gen(length, resolve) {
let count = 0;
let values = [];
return function(i, value) {
values[i] = value;
if (++count === length) {
resolve(values);
}
}
}
return new Promise1((resolve, reject) => {
let done = gen(promises.length, resolve);
promises.forEach((promise, index) => {
promise.then((value) => {
done(index, value)
}, reject)
})
})
}
//Promise.race方法
Promise1.race = function(promises) {
return new Promise1((resolve, reject) => {
promises.forEach((promise, index) => {
promise.then(resolve, reject);
});
});
}
複製程式碼
這裡有個問題,就是在當我們console.log(Promise1.resolve('a'))的時候,我發現列印出來的狀態竟然是 pending狀態,我猜想原因是應該是resolve中函式非同步執行,在當我們console的時候setTimeout中程式碼未執行,所以我給出的解決方法是將狀態變化與賦值移到setTimeout外面,這樣就不會產生剛才的問題了,更改後程式碼長這樣:
function resolve(value) {
if(value instanceof Promise) {
value.then(resolve,reject);
}
self.status = FULFILLED;
self.value = value;
setTimeout(function(){
if(self.status===PENDING){
self.fullfilledCallbacks.forEach(function(cb){
cb(self.value)
})
}
})
}
function reject(reason) {
self.status = REJECTED;
self.reason = reason;
setTimeout(function(){
if(self.status===PENDING) {
self.rejectedCallbacks.forEach(function(cb){
cb(self.reason);
})
}
})
}
複製程式碼
總結
經過以上實踐,我們成功的手寫了一個功能完備的 Promise。這裡給我的最大啟發是如果我們想學習一個東西,必須深入到它的底層,瞭解它的執行原理與具體實現方法,並且可以造一個簡單的輪子,這樣才算我們掌握了該知識點。從前的我對於這一點沒有關注的太多,導致在用某個知識點時只是掌握的它的表層用法,在高階一點的使用場景時完全不會運用。以後我會更加註重原始碼方面的學習,彌補我這方面的不足。