本文寫給有必定Promise使用經驗的人,若是你尚未使用過Promise,這篇文章可能不適合你,建議先瞭解Promise的使用javascript
本篇文章總體架構的大圖以下,接下來會一步一步去實現一個 Promise
。html
首先呢,promise確定是一個類,同時還定義了 resolve
和 reject
方法。html5
function Promise(executor) {
// 初始化state爲等待態
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失敗的緣由
this.reason = undefined;
// 存放 fn1 的回調
this.fn1Callbacks = [];
// 存放 fn2 的回調
this.fn2Callbacks = [];
// 成功
let resolve = () => { };
// 失敗
let reject = () => { };
// 當即執行
executor(resolve, reject);
}
複製代碼
上面的代碼實現了Promise
構造函數的主體,但有兩個問題:java
executor
有可能會出錯,對吧,畢竟是用戶傳進來的方法,相似下面這樣。若是executor出錯,報錯咱們須要用 try catch 捕獲一下,Promise應該被其throw出的值reject:es6
new Promise(function(resolve, reject) {
console.log(a) // a 沒有被定義
})
複製代碼
resolve
、reject
仍是空函數,咱們須要在裏面補上邏輯。數組
接下來繼續完善:promise
function Promise(executor){
// 初始化state爲等待態
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失敗的緣由
this.reason = undefined;
let resolve = value => {
// state改變,resolve調用就會失敗
if (this.state === 'pending') {
// resolve調用後,state轉化爲成功態
this.state = 'fulfilled';
// 儲存成功的值
this.value = value;
}
};
let reject = reason => {
// state改變,reject調用就會失敗
if (this.state === 'pending') {
// reject調用後,state轉化爲失敗態
this.state = 'rejected';
// 儲存失敗的緣由
this.reason = reason;
}
};
// 若是executor執行報錯,直接執行reject
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
複製代碼
用一張圖小結一下:架構
上面的代碼不算特別複雜,下面的then
方法有點複雜。異步
Promise
對象有一個then
方法,用來註冊在這個Promise狀態肯定後的回調。當Promise
的狀態發生了改變,不管是成功或是失敗都會調用then
方法async
then
方法使用方法以下:
// then 方法傳入兩個方法做爲參數,一個是fn1方法,一個是 fn2 方法
p1.then(function fn1(data){
// fn1 方法的參數,用於獲取promise對象的值
}, function fn2(err){
// fn1 方法的參數,用於獲取失敗的緣由
})
複製代碼
從上面的例子,很明顯,咱們得出結論:
then
方法能夠在 p1
實例上調用。所以then
方法的實現是在Promise
的 prototype
上。
then
方法會返回一個Promise
,並且是返回一個新的Promise(詳情)對象。
能夠屢次調用then
方法,也就是鏈式調用,而且每次會返回一個新Promise
對象,Promise
狀態是不肯定的,多是 fullfilled
, 也多是 resolve
, 取決於那一次調用then
時, fn1
的返回值。
因此,then
方法的實現也很簡單,根據Promise
狀態來調用不一樣的回調函數便可
下面是 then 方法的思路圖:
下面咱們來實現 then
方法:
// then方法接收兩個參數,fn1,fn2,分別爲Promise成功或失敗後的回調
Promise.prototype.then = function(fn1, fn2) {
var self = this
var promise2
// 首先對入參 fn1, fn2作判斷
fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
//todo
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
//todo
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
// todo
})
}
}
複製代碼
首先,對入參 fn1
, fn2
作判斷。規範中說,fn1
和 fn2
都是可選參數。
也就是說能夠傳也能夠不傳。傳入的回調函數也不是一個函數類型,那怎麼辦?規範中說忽略它就行了。所以須要判斷一下回調函數的類型,若是明確是個函數再執行它。
其次,Promise
總共有三種可能的狀態,咱們分三個if
塊來處理,在裏面分別都返回一個new Promise。
因此,接下來的邏輯是:
若是 promise
狀態是 resolved
,須要執行 fn1
;
若是 promise
狀態是 rejected
, 須要執行fn2
;
若是 promise
狀態是 pending
, 咱們並不能肯定調用 fn1
仍是 fn2
,只能先把方法都保存在 fn1Callback
, fn2Callback
數組中。等到Promise的狀態肯定後再處理。
根據上面的邏輯,填充下面代碼:
Promise.prototype.then = function(fn1, fn2) {
var self = this
var promise2
fn1 = typeof fn1 === 'function' ? fn1 : function(v) {}
fn2 = typeof fn2 === 'function' ? fn2 : function(r) {}
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
// 把 fn一、fn2 放在 try catch 裏面,畢竟 fn一、fn2 是用戶傳入的,報錯嘛,很常見
try {
var x = fn1(self.data)
// fn1 執行後,會有返回值,經過 resolve 注入到 then 返回的 promise 中
resolve(x)
} catch (e) {
reject(e)
}
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
try {
var x = fn2(self.data)
reject(x)
} catch (e) {
reject(e)
}
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
this.fn1Callback.push(function(value){
try {
var x = fn1(self.data);
resolve(x)
} catch (e) {
reject(e)
}
})
this.fn2Callback.push(function(value) {
try {
var x = fn2(self.data);
reject(x)
} catch (e) {
reject(e)
}
})
})
}
}
複製代碼
fn1
, fn2
都是用戶傳入的,有可能報錯唉,因此要放在 try catch 裏面
fn1
, fn2
的返回值,咱們記爲 x
, 規範中的命名也是 x
, 保持一致。 x
值將在下文中頻繁使用。
then
函數本質是把fn1
的返回值,包裝成一個 promise
返回出去。問題是,fn1
的返回值是開發者寫的,可能千奇百怪。上面代碼中,假定 x
是一個普通值。其實實際上,x
有不一樣的狀況,咱們得去分別處理:
若是 x
是一個普通值,如同上面的代碼,直接使用 resolve
方法,then
就能夠返回一個正常的promise
return new Promise((resolve) => {
var x = fn1(self.data);
resolve(x)
})
複製代碼
若是 x
是一個 promise ,須要等待這個 promise
狀態變化, 拿到fullfilled
的值。而後咱們代碼再改一改,增長一個判斷
return new Promise((resolve) => {
var x = fn1(self.data);
if (x instanceof Promise) {
x.then((data) => {resolve(data)}, (e) => {reject(e)})
} else {
resolve(x)
}
})
複製代碼
根據規定,咱們須要兼容各類百花齊放的寫法,好比說,若是 x
是一個對象,而且對象有 then
方法,也就是所謂的 thenable
對象,則咱們得這樣處理:
return new Promise((resolve) => {
var x = fn1(self.data);
if (x instanceof Promise) {
x.then((data) => {resolve(data)}, (e) => {reject(e)})
} else if (typeof x.then === 'function'){
x.then(function(y){
resolve(y)
}, function(e){
reject(e)
})
} else {
resolve(x)
}
})
複製代碼
上面,咱們新增了一些邏輯,爲了處理 x 返回值 的各類狀況。咱們須要把這些邏輯,挪到一個 resolvePromise
方法中,resolvePromise
負責把 各類稀奇古怪的 x
包裝成一個正常的 promise
。
resolvePromise
方法,就是爲了把 x
包裹成一個正常的promise
function resolvePromise(promise2, x, resolve, reject) {
// 爲了防止循環引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'));
}
// 若是 x 是 promise
if (x instanceof Promise) {
x.then(function (data) {
resolve(data)
}, function (e) {
reject(e)
});
return;
}
// 若是 x 是 object 類型或者是 function
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
// 拿x.then可能會報錯
try {
// 先拿到 x.then
var then = x.then;
var called
if (typeof then === 'function') {
// 這裏的寫法,是 then.call(this, fn1, fn2)
then.call(x, (y) => {
// called 是幹什麼用的呢?
// 有一些 promise 實現的不是很規範,瞎搞的,好比說,fn1, fn2 本應執行一個,
// 可是有些then實現裏面,fn1, fn2都會執行
// 爲了 fn1 和 fn2 只能調用一個, 設置一個 called 標誌位
if (called) {
return;
}
called = true;
return resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) {
return;
}
called = true;
return reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) {
return;
}
return reject(e);
}
} else {
resolve(x);
}
}
複製代碼
上面代碼,須要注意的地方:
var then = x.then
這一行代碼可能會報錯,須要用 try catch 包一下。爲何取對象上的屬性有報錯的可能?Promise
有不少實現(bluebird
,Q等),Promises/A+只是一個規範,你們都按此規範來實現Promise纔有可能通用,所以全部出錯的可能都要考慮到,假設另外一我的實現的Promise對象使用Object.defineProperty()
惡意的在取值時拋錯,咱們能夠防止代碼出現Bug。
若是對象中有then,且then是函數類型,就能夠認爲是一個Promise對象,以後,使用x
做爲this來調用then方法。
若是 x === promise2
,則是會形成循環引用,本身等待本身完成,則報「循環引用」錯誤。 x 和 promise2 是同一個是什麼狀況呢?
let p2 = p1.then(function(data){
console.log(data)
return p2;
})
複製代碼
上面的例子中,p1.then()
的返回值是p2
,fn1
的返回值也是 p2
。這會存在什麼問題呢?promise
若是不手動的調用 resolve
方法,是沒有辦法修改狀態的。p2
的狀態無法改變,無法本身改動本身的狀態,永遠不會被 fullfilled
、rejected
咱們須要不一樣的Promise實現可以相互交互,即咱們要把fn1
/ fn2
的返回值,x,當成一個多是Promise的對象,也即標準裏所說的thenable
,並以最保險的方式調用x上的then方法。若是你們都按照標準實現,那麼不一樣的Promise之間就能夠交互了。而標準爲了保險起見,即便x返回了一個帶有then屬性但並不遵循Promise標準的對象(好比說這個x把它then裏的兩個參數都調用了,同步或者異步調用(PS,原則上then的兩個參數須要異步調用,下文會講到),或者是出錯後又調用了它們,或者then根本不是一個函數),也能儘量正確處理。
最後,咱們剛剛說到,原則上,promise.then(onResolved, onRejected)
裏的這兩相函數須要異步調用,關於這一點,標準裏也有說明:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.
那麼如何將同步代碼變成異步執行呢?可使用setTimeout函數來模擬一下:
setTimeout(()=>{
//此入的代碼會異步執行
},0);
複製代碼
利用此技巧,將代碼then執行處的全部地方使用setTimeout變爲異步便可,舉個栗子:
setTimeout(() => {
try {
let x = fn1(value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
},0);
複製代碼
// 1. 定義 status 狀態
// 2. fn1, fn2 的數組
// 3. 定義 resolve reject 方法
// 4. executor 執行
function Promise(executor) {
let self = this;
self.status = 'pending';
self.fn1Callback = [];
self.fn2Callback = [];
// resolve 作到事情
// 1. 修改this 實例的狀態
// 2. 修改this 這裏的data
// 3. 遍歷執行 this fn1Callback 上掛載的方法
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
setTimeout(() => { // 異步執行全部的回調函數
if (self.status === 'pending') {
self.status = 'resolved';
self.data = value;
for (let i = 0; i < self.fn1Callback.length; i++) {
self.fn1Callback[i](value);
}
}
});
}
function reject(reason) {
setTimeout(() => { // 異步執行全部的回調函數
if (self.status === 'pending') {
self.status = 'rejected';
self.data = reason;
for (let i = 0; i < self.fn2Callback.length; i++) {
self.fn2Callback[i](reason);
}
}
});
}
try {
executor(resolve, reject);
} catch (reason) {
reject(reason);
}
}
// 1. 參數校驗
// 2. 根據 statue, 執行 fn1, fn2 或者把 執行fn1, fn2的行爲保存在數組
// 3. 把 fn1,fn2 的返回值, 使用 resolvePromise 包裹成 promise
Promise.prototype.then = function (fn1, fn2) {
let self = this;
let promise2;
fn1 = typeof fn1 === 'function' ? fn1 : function (v) {
return v;
};
fn2 = typeof fn2 === 'function' ? fn2 : function (r) {
throw r;
};
// 執行到 then, 並不肯定 promise 狀態已是 resolved
if (self.status === 'resolved') {
// then() 執行後,返回一個promise, promise 的值
return promise2 = new Promise(((resolve, reject) => {
setTimeout(() => { // 異步執行onResolved
try {
// 執行 fn1(),拿到結果 x
// fn1是用戶傳入的,那fn1返回值, 可能性可就多了
let x = fn1(self.data);
// 若是 x 是簡單值,直接 resolve(x);
// resolve(x);
// 須要使用 resolvePromise 方法封裝
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.status === 'rejected') {
return promise2 = new Promise(((resolve, reject) => {
setTimeout(() => { // 異步執行onRejected
try {
let x = fn2(self.data);
resolvePromise(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.status === 'pending') {
// 這裏之因此沒有異步執行,是由於這些函數必然會被resolve或reject調用,而resolve或reject函數裏的內容已經是異步執行,構造函數裏的定義
return promise2 = new Promise(((resolve, reject) => {
// 先定義一個方法,把方法 掛載到 onResolvedCallback 數組上
// 方法裏面 就是 調用傳入的 fn1
self.onResolvedCallback.push((value) => {
try {
let x = fn1(value);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.onRejectedCallback.push((reason) => {
try {
let x = fn2(reason);
resolvePromise(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 1. 普通值
// 2. promise 值
// 3. thenable 的值,執行 then
function resolvePromise(promise2, x, resolve, reject) {
// 爲了防止循環引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'));
}
// 若是 x 是 promise
if (x instanceof Promise) {
x.then(function (data) {
resolve(data)
}, function (e) {
reject(e)
});
return;
}
// 若是 x 是 object 類型或者是 function
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
// 拿x.then可能會報錯
try {
// 先拿到 x.then
var then = x.then;
var called
if (typeof then === 'function') {
// 這裏的寫法,是 then.call(this, fn1, fn2)
then.call(x, (y) => {
// called 是幹什麼用的呢?
// 有一些 promise 實現的不是很規範,瞎搞的,好比說,fn1, fn2 本應執行一個,
// 可是有些then實現裏面,fn1, fn2都會執行
// 爲了 fn1 和 fn2 只能調用一個, 設置一個 called 標誌位
if (called) {
return;
}
called = true;
return resolvePromise(promise2, y, resolve, reject);
}, (r) => {
if (called) {
return;
}
called = true;
return reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) {
return;
}
return reject(e);
}
} else {
resolve(x);
}
}
複製代碼
Promise.all
須要等到全部的 promise
的狀態都變成 fulfilled
以後才 resolve
, 但只要有一個 promise
失敗即返回失敗的結果。
Promise.all = function (arr) {
return new Promise((resolve, reject) => {
if (!Array.isArray(arr)) {
throw new Error(`argument must be a array`)
}
let dataArr = [];
let num = 0;
for (let i = 0; i < arr.length; i++) {
let p = arr[i];
p.then((data) => {
dataArr.push(data);
num ++;
if (num === arr.length) {
return resolve(data)
}
}).catch((e) => {
return reject(e)
})
}
})
}
複製代碼
retry 是報錯會嘗試,嘗試超過必定次數才真正的 reject
Promise.retry = function(getData, times, delay) {
return new Promise((resolve, reject) => {
function attemp() {
getData().then((data) => {
resolve(data)
}).catch((err) => {
if (times === 0) {
reject(err)
} else {
times--
setTimeout(attemp, delay)
}
})
}
attemp()
})
}
複製代碼
好不容易寫好的Promise源碼,最終是否真的符合Promises/A+規範,開源社區提供了一個包用於測試咱們的代碼:promises-aplus-tests
此包能夠一項項的檢查咱們寫的代碼是否合規,若是有任一項不符就會給咱們報出來,若是檢查你的代碼一路都是綠色,那恭喜,你的Proimse已經合法了,能夠上線提供給別人使用了。