本文首發于 vivo網際網路技術 微信公衆号
連結:
https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ 作者:孔垂亮
很多同學在學習 Promise 時,知其然卻不知其是以然,對其中的用法了解不了。本系列文章由淺入深逐漸實作 Promise,并結合流程圖、執行個體以及動畫進行示範,達到深刻了解 Promise 用法的目的。
本系列文章有如下幾個章節組成:
- 圖解 Promise 實作原理(一)—— 基礎實作
- 圖解 Promise 實作原理(二)—— Promise 鍊式調用
- 圖解 Promise 實作原理(三)—— Promise 原型方法實作
- 圖解 Promise 實作原理(四)—— Promise 靜态方法實作
本文适合對 Promise 的用法有所了解的人閱讀,如果還不清楚,請自行查閱阮一峰老師的 《
ES6入門 之 Promise 對象》。
Promise 規範有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的更新版 Promise/A+,有興趣的可以去了解下,最終 ES6 中采用了 Promise/A+ 規範。是以本文的Promise源碼是按照
Promise/A+規範來編寫的(不想看英文版的移步
Promise/A+規範中文翻譯)。
引子
為了讓大家更容易了解,我們從一個場景開始,一步一步跟着思路思考,會更容易看懂。
考慮下面一種擷取使用者 id 的請求處理:
//不使用Promise
http.get('some_url', function (result) {
//do something
console.log(result.id);
});
//使用Promise
new Promise(function (resolve) {
//異步請求
http.get('some_url', function (result) {
resolve(result.id)
})
}).then(function (id) {
//do something
console.log(id);
})
乍一看,好像不使用 Promise 更簡潔一些。其實不然,設想一下,如果有好幾個依賴的前置請求都是異步的,此時如果沒有 Promise ,那回調函數要一層一層嵌套,看起來就很不舒服了。如下:
//不使用Promise
http.get('some_url', function (id) {
//do something
http.get('getNameById', id, function (name) {
//do something
http.get('getCourseByName', name, function (course) {
//dong something
http.get('getCourseDetailByCourse', function (courseDetail) {
//do something
})
})
})
});
//使用Promise
function getUserId(url) {
return new Promise(function (resolve) {
//異步請求
http.get(url, function (id) {
resolve(id)
})
})
}
getUserId('some_url').then(function (id) {
//do something
return getNameById(id); // getNameById 是和 getUserId 一樣的Promise封裝。下同
}).then(function (name) {
//do something
return getCourseByName(name);
}).then(function (course) {
//do something
return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
//do something
});
實作原理
說到底,Promise 也還是使用回調函數,隻不過是把回調封裝在了内部,使用上一直通過 then 方法的鍊式調用,使得多層的回調嵌套看起來變成了同一層的,書寫上以及了解上會更直覺和簡潔一些。
一、基礎版本
//極簡的實作
class Promise {
callbacks = [];
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
}
_resolve(value) {
this.callbacks.forEach(fn => fn(value));
}
}
//Promise應用
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('5秒');
}, 5000);
}).then((tip) => {
console.log(tip);
})
上述代碼很簡單,大緻的邏輯是這樣的:
- 調用 then 方法,将想要在 Promise 異步操作成功時執行的 onFulfilled 放入callbacks隊列,其實也就是注冊回調函數,可以向觀察者模式方向思考;
- 建立 Promise 執行個體時傳入的函數會被賦予一個函數類型的參數,即 resolve,它接收一個參數 value,代表異步操作傳回的結果,當異步操作執行成功後,會調用resolve方法,這時候其實真正執行的操作是将 callbacks 隊列中的回調一一執行。

(圖:基礎版本實作原理)
首先 new Promise 時,傳給 Promise 的函數設定定時器模拟異步的場景,接着調用 Promise 對象的 then 方法注冊異步操作完成後的 onFulfilled,最後當異步操作完成時,調用 resolve(value), 執行 then 方法注冊的 onFulfilled。
then 方法注冊的 onFulfilled 是存在一個數組中,可見 then 方法可以調用多次,注冊的多個onFulfilled 會在異步操作完成後根據添加的順序依次執行。如下:
//then 的說明
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('5秒');
}, 5000);
});
p.then(tip => {
console.log('then1', tip);
});
p.then(tip => {
console.log('then2', tip);
});
上例中,要先定義一個變量 p ,然後 p.then 兩次。而規範中要求,then 方法應該能夠鍊式調用。實作也簡單,隻需要在 then 中 return this 即可。如下所示:
//極簡的實作+鍊式調用
class Promise {
callbacks = [];
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
return this;//看這裡
}
_resolve(value) {
this.callbacks.forEach(fn => fn(value));
}
}
let p = new Promise(resolve => {
setTimeout(() => {
console.log('done');
resolve('5秒');
}, 5000);
}).then(tip => {
console.log('then1', tip);
}).then(tip => {
console.log('then2', tip);
});
(圖:基礎版本的鍊式調用)
二、加入延遲機制
上面 Promise 的實作存在一個問題:如果在 then 方法注冊 onFulfilled 之前,resolve 就執行了,onFulfilled 就不會執行到了。比如上面的例子中我們把 setTimout 去掉:
//同步執行了resolve
let p = new Promise(resolve => {
console.log('同步執行');
resolve('同步執行');
}).then(tip => {
console.log('then1', tip);
}).then(tip => {
console.log('then2', tip);
});
執行結果顯示,隻有 "同步執行" 被列印了出來,後面的 "then1" 和 "then2" 均沒有列印出來。再回去看下 Promise 的源碼,也很好了解,resolve 執行時,callbacks 還是空數組,還沒有onFulfilled 注冊上來。
這顯然是不允許的,Promises/A+規範明确要求回調需要通過異步方式執行,用以保證一緻可靠的執行順序。是以要加入一些處理,保證在 resolve 執行之前,then 方法已經注冊完所有的回調:
//極簡的實作+鍊式調用+延遲機制
class Promise {
callbacks = [];
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
this.callbacks.push(onFulfilled);
return this;
}
_resolve(value) {
setTimeout(() => {//看這裡
this.callbacks.forEach(fn => fn(value));
});
}
}
在 resolve 中增加定時器,通過 setTimeout 機制,将 resolve 中執行回調的邏輯放置到JS任務隊列末尾,以保證在 resolve 執行時,then方法的 onFulfilled 已經注冊完成。
(圖:延遲機制)
但是這樣依然存在問題,在 resolve 執行後,再通過 then 注冊上來的 onFulfilled 都沒有機會執行了。如下所示,我們加了延遲後,then1 和 then2 可以列印出來了,但下例中的 then3 依然列印不出來。是以我們需要增加狀态,并且儲存 resolve 的值。
let p = new Promise(resolve => {
console.log('同步執行');
resolve('同步執行');
}).then(tip => {
console.log('then1', tip);
}).then(tip => {
console.log('then2', tip);
});
setTimeout(() => {
p.then(tip => {
console.log('then3', tip);
})
});
三、增加狀态
為了解決上一節抛出的問題,我們必須加入狀态機制,也就是大家熟知的 pending、fulfilled、rejected。
Promises/A+ 規範中明确規定了,pending 可以轉化為 fulfilled 或 rejected 并且隻能轉化一次,也就是說如果 pending 轉化到 fulfilled 狀态,那麼就不能再轉化到 rejected。并且 fulfilled 和 rejected 狀态隻能由 pending 轉化而來,兩者之間不能互相轉換。
增加狀态後的實作是這樣的
//極簡的實作+鍊式調用+延遲機制+狀态
class Promise {
callbacks = [];
state = 'pending';//增加狀态
value = null;//儲存結果
constructor(fn) {
fn(this._resolve.bind(this));
}
then(onFulfilled) {
if (this.state === 'pending') {//在resolve之前,跟之前邏輯一樣,添加到callbacks中
this.callbacks.push(onFulfilled);
} else {//在resolve之後,直接執行回調,傳回結果了
onFulfilled(this.value);
}
return this;
}
_resolve(value) {
this.state = 'fulfilled';//改變狀态
this.value = value;//儲存結果
this.callbacks.forEach(fn => fn(value));
}
}
注意:當增加完狀态之後,原先的_resolve中的定時器可以去掉了。當reolve同步執行時,雖然callbacks為空,回調函數還沒有注冊上來,但沒有關系,因為後面注冊上來時,判斷狀态為fulfilled,會立即執行回調。
(圖:Promise 狀态管理)
實作源碼中隻增加了 fulfilled 的狀态 和 onFulfilled 的回調,但為了完整性,在示意圖中增加了 rejected 和 onRejected 。後面章節會實作。
resolve 執行時,會将狀态設定為 fulfilled ,并把 value 的值存起來,在此之後調用 then 添加的新回調,都會立即執行,直接傳回儲存的value值。
(Promise 狀态變化示範動畫)
詳情請點選:
至此,一個初具功能的Promise就實作好了,它實作了 then,實作了鍊式調用,實作了狀态管理等等。但仔細想想,鍊式調用的實作隻是在 then 中 return 了 this,因為是同一個執行個體,調用再多次 then 也隻能傳回相同的一個結果,這顯然是不能滿足我們的要求的。下一節,講述如何實作真正的鍊式調用。