Promise 原始碼:實現一個簡單的 Promise

cobish發表於2018-05-21

前言

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。

更詳細的步驟是這樣子的:

  1. new Promise,執行 Promise 建構函式;
  2. 建構函式裡,執行 a 函式;
  3. 執行 then 函式;
  4. 1 秒後,執行 resolve 函式;
  5. 執行 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 裡,有兩個屬性,分別是 isResolvedcallback。isResolved 是一個標識,用來防止多次呼叫 resolve。callback 是一個陣列,用來儲存回撥函式。

MyPromise 還有一個 then 函式,用來處理非同步後的回撥,能夠鏈式非同步呼叫。

這樣子就實現了一個簡單的 Promise,完整的程式碼戳 這裡

來源:https://juejin.im/post/5b02ae25f265da0ba6101c72

相關文章