前言
Promise 是 ES6 新增的一個內建物件, 它是用來避免回撥地獄的一種解決方案。
從以前一直巢狀傳回撥函式,到使用 Promise 來鏈式非同步回撥。Promise 究竟是怎麼實現,從而達到回撥函式“扁平化”?
接下來就來一步步實現一個簡單的 Promise。開始發車了…
執行步驟
先來看一個使用 Promise 的簡單例子:
var p = new Promise(function a (resolve) {
console.log(1);
setTimeout(function () {
resolve(2);
}, 1000);
})p.then(function b (val) {
console.log(val);
});
複製程式碼
程式碼執行,它會先執行函式 a,列印出 1。定時器 1 秒後執行 resolve,緊接著執行了函式 b。
更詳細的步驟是這樣子的:
- new Promise,執行 Promise 建構函式;
- 建構函式裡,執行 a 函式;
- 執行 then 函式;
- 1 秒後,執行 resolve 函式;
- 執行 b 函式。
這裡的一個思路就是,在 then 函式執行時用一個屬性儲存函式 b,然後在 resolve 執行時再將其執行。
開始封裝
這裡定義一個 MyPromise,它有 then 函式,還有一個 callback
屬性用來儲存上面的“b 函式”。
function MyPromise (fn) {
var _this = this;
// 用來儲存 then 傳入的回撥函式 this.callback = undefined;
function resolve (val) {
_this.callback &
&
_this.callback(val);
} fn(resolve);
}MyPromise.prototype.then = function (cb) {
this.callback = cb;
};
複製程式碼
測試使用:
var p = new MyPromise(function (resolve) {
console.log(1);
setTimeout(function () {
resolve(2);
}, 1000);
});
p.then(function (val) {
console.log(val);
});
複製程式碼
程式碼執行時會馬上列印出 1,1秒後列印出 2。毛問題。
多個 resolve 呼叫處理
上面已經實現了一個簡單的 Promise,當然還有很多種情況需要考慮。
比如會有這麼一種情況,呼叫了多個 resolve 函式:
var p = new MyPromise(function (resolve) {
console.log(1);
setTimeout(function () {
resolve(2);
resolve(3);
resolve(4);
}, 1000);
});
複製程式碼
原生的 Promise 在呼叫了第一個 resolve 之後,後面的 resolve 都無效化,即後面的 resolve 都是沒用的程式碼。
這裡的處理方式是,給 MyPromise 再新增一個屬性 isResolved
,用來記錄是否呼叫過 resolve 函式。如果呼叫過,用它標識一下。再有 resolve 的呼叫,則用它判斷返回。
function MyPromise (fn) {
var _this = this;
this.callback = undefined;
this.isResolved = false;
function resolve (val) {
if (_this.isResolved) return;
_this.isResolved = true;
_this.callback &
&
_this.callback(val);
} fn(resolve);
}複製程式碼
多個 then 處理
繼續走,除了可以呼叫多個 resolve 函式,同樣我們可以呼叫多個 then 函式。
var p = new MyPromise(function (resolve) {
console.log(1);
setTimeout(function () {
resolve(2);
}, 1000);
});
p.then(function (val) {
console.log(val);
});
p.then(function (val) {
console.log(val);
});
複製程式碼
與 resolve 不同,這裡的每一個傳給 then 的回撥函式都會在 1 秒後執行,即 then 函式都有效。程式碼執行,先列印出 1。1 秒後,列印兩個 2。
所以 MyPromise 的 callback 屬性需改成陣列
的格式,儲存著每一個 then 的回撥函式。
function MyPromise (fn) {
var _this = this;
this.callback = [];
this.isResolved = false;
function resolve (val) {
if (_this.isResolved) return;
_this.isResolved = true;
if (_this.callback.length >
0) {
_this.callback.forEach(function (func) {
func &
&
func(val);
});
}
} fn(resolve);
}MyPromise.prototype.then = function (cb) {
this.callback.push(cb);
};
複製程式碼
在多次呼叫 then 時,MyPromise 通過屬性 callback 來儲存多個回撥函式。在 resolve 執行後,再去遍歷 callback,將它儲存的回撥函式逐個執行。
支援 then 鏈式呼叫
Promise 相對於回撥地獄而言,它的優勢在於可以進行 then 的鏈式呼叫,從而將回撥函式“扁平化”。
比如我有一個非同步操作需要在另一個非同步操作後才能執行,只需要繼續呼叫 then 就能夠下一步回撥。
var p = new MyPromise(function (resolve) {
console.log(1);
setTimeout(function () {
resolve(2);
}, 1000);
});
p.then(function (val) {
console.log(val);
}).then(function (val) {
console.log(val);
});
複製程式碼
既然要能夠 then 鏈式呼叫,那我在執行完 then 函式後返回 this 不就可以啦。但這不就跟剛剛的程式碼一樣了嗎?
p.then(function (val) {
console.log(val);
});
p.then(function (val) {
console.log(val);
});
複製程式碼
這樣就會在呼叫 resolve 函式之後同時執行,而不是執行完第一個 then 後,再執行第二個。所以直接返回 this 的方案是不行的。
我們再想,還有什麼可以返回的,並且帶有 then 函式的。答案就是 new 一個新的 MyPromise 並返回。我們需重寫一個 then 函式的實現。
MyPromise.prototype.then = function (cb) {
var _this = this;
return new MyPromise(function (resolve) {
_this.callback.push({
cb: cb, resolve: resolve
});
});
};
複製程式碼
這裡 callback 重新改寫了一下,儲存的是 then 的回撥函式,和新 new 的 MyPromise 的 resolve 函式。儲存的 resolve 函式先不執行,因為我們知道,它一旦執行了,就會觸發傳入 then 的回撥函式的執行。
同時,MyPromise 建構函式裡的 resolve 也需要調整一下:
function MyPromise (fn) {
var _this = this;
this.callback = [];
this.isResolved = false;
function resolve (val) {
if (_this.isResolved) return;
_this.isResolved = true;
if (_this.callback.length >
0) {
_this.callback.forEach(function (item) {
var res;
var cb = item.cb;
var resolve = item.resolve;
cb &
&
(res = cb(val));
resolve &
&
resolve(res);
});
}
} fn(resolve);
}複製程式碼
在執行第一個 then 的回撥函式時,將其執行完返回的值,作為儲存的 resolve 的引數傳入。
p.then(function (val) {
console.log(val);
return val + 1;
}).then(function(val) {
console.log(val);
});
複製程式碼
這樣子,就能夠鏈式的 then 呼叫。先別急,我們實現的只是 then 的同步
鏈式呼叫,而我們最終要的是非同步
的鏈式呼叫。
我們需要這樣子的:
p.then(function (val) {
console.log(val);
return new MyPromise(function (resolve) {
setTimeout(function () {
resolve(val + 1);
}, 1000);
});
}).then(function(val) {
console.log(val);
});
複製程式碼
先列印出 1,1秒後列印出第一個 then 裡的 2,再過多一秒,列印出第二個 then 的 3。
所以,我們需要在取出原來儲存的 cb 返回的值進行判斷。如果該值是一個 MyPromise 物件,則呼叫它的 then,否則跟原來一樣呼叫。
function MyPromise (fn) {
var _this = this;
this.callback = [];
this.isResolved = false;
function resolve (val) {
if (_this.isResolved) return;
_this.isResolved = true;
if (_this.callback.length >
0) {
_this.callback.forEach(function (item) {
var res;
var cb = item.cb;
var resolve = item.resolve;
cb &
&
(res = cb(val));
if (typeof res === 'object' &
&
res.then) {
res.then(resolve);
} else {
resolve &
&
resolve(res);
}
});
}
} fn(resolve);
}複製程式碼
最後
在我們實現的 MyPromise 裡,有兩個屬性,分別是 isResolved
和 callback
。isResolved 是一個標識,用來防止多次呼叫 resolve。callback 是一個陣列,用來儲存回撥函式。
MyPromise 還有一個 then 函式,用來處理非同步後的回撥,能夠鏈式非同步呼叫。
這樣子就實現了一個簡單的 Promise,完整的程式碼戳 這裡。