多是目前最易理解的手寫promise

引子

本文寫給有必定Promise使用經驗的人,若是你尚未使用過Promise,這篇文章可能不適合你,建議先瞭解Promise的使用javascript

本篇文章總體架構的大圖以下,接下來會一步一步去實現一個 Promisehtml

Promise 類

首先呢,promise確定是一個類,同時還定義了 resolvereject 方法。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

  1. executor有可能會出錯,對吧,畢竟是用戶傳進來的方法,相似下面這樣。若是executor出錯,報錯咱們須要用 try catch 捕獲一下,Promise應該被其throw出的值reject:es6

    new Promise(function(resolve, reject) {
      console.log(a)  // a 沒有被定義
    })
    複製代碼
  2. resolvereject 仍是空函數,咱們須要在裏面補上邏輯。數組

接下來繼續完善: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 方法有點複雜。異步

實現 then 方法

Promise對象有一個then方法,用來註冊在這個Promise狀態肯定後的回調。當Promise的狀態發生了改變,不管是成功或是失敗都會調用then方法async

then 方法使用方法以下:

// then 方法傳入兩個方法做爲參數,一個是fn1方法,一個是 fn2 方法
p1.then(function fn1(data){
    // fn1 方法的參數,用於獲取promise對象的值
}, function fn2(err){
    // fn1 方法的參數,用於獲取失敗的緣由
})
複製代碼

從上面的例子,很明顯,咱們得出結論:

  1. then方法能夠在 p1 實例上調用。所以then 方法的實現是在Promiseprototype上。

  2. then方法會返回一個Promise,並且是返回一個新的Promise(詳情)對象。

  3. 能夠屢次調用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作判斷。規範中說,fn1fn2 都是可選參數。

也就是說能夠傳也能夠不傳。傳入的回調函數也不是一個函數類型,那怎麼辦?規範中說忽略它就行了。所以須要判斷一下回調函數的類型,若是明確是個函數再執行它。

其次,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

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() 的返回值是p2fn1的返回值也是 p2。這會存在什麼問題呢?promise 若是不手動的調用 resolve 方法,是沒有辦法修改狀態的。p2 的狀態無法改變,無法本身改動本身的狀態,永遠不會被 fullfilledrejected

  • 咱們須要不一樣的Promise實現可以相互交互,即咱們要把fn1 / fn2的返回值,x,當成一個多是Promise的對象,也即標準裏所說的thenable,並以最保險的方式調用x上的then方法。若是你們都按照標準實現,那麼不一樣的Promise之間就能夠交互了。而標準爲了保險起見,即便x返回了一個帶有then屬性但並不遵循Promise標準的對象(好比說這個x把它then裏的兩個參數都調用了,同步或者異步調用(PS,原則上then的兩個參數須要異步調用,下文會講到),或者是出錯後又調用了它們,或者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);
複製代碼

promise 主體結構

// 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.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)
            })
        }
    })
}
複製代碼

手寫 Promise.retry

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已經合法了,能夠上線提供給別人使用了。

相關文章
相關標籤/搜索