手寫一個自己的Promise

zxyling發表於2018-07-29

這裡我們先囉嗦一下Promise的概念:

什麼是promise?

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。

那如何實現一個符合規範的Promise呢?

手寫一個自己的Promise

參考promiseA+規範總結:

我們知道promise中共有三種狀態 pending 過渡態 fulfilled 完成態 rejected 失敗態

promise狀態改變只有兩種可能

  • 過渡態=>成功態

  • 過渡態 => 失敗態

過程不可逆 無法相互轉化

這裡來借用一張圖片更容易理解

image

let promise = new Promise((resolve, reject) => {
    //這裡放入我們要執行的函式,可能是同步,也可能是非同步, 這裡我們就來寫一個非同步的執行
    setTimeout(() => {
        resolve('hello');
    })
})
    
promise.then(data => {
    console.log(data);
}, err => {console.log(err)})
複製程式碼

上面程式碼表示我們new一個promise例項,並非同步執行 這裡通過呼叫then方法,我們成功得到了結果

第一步 觀察語法

觀察原生promise用法,我們可以發現,在new Promise時候傳入了一個函式,這個函式在規範中的叫法是exector 執行器 看到這裡,我們先有一個大概思路,構建一個自己的Promise建構函式

// 這裡我們建立了一個建構函式 引數就是執行器
function Promise(exector) {
    
}
複製程式碼

好的,第一步完成, 重點來了,這個Promise內部到底幹了什麼呢 可以看到,原生的exector中傳入了兩個引數,第一個引數執行會讓promise狀態變為resolve, 也就是成功, 第二個執行會讓函式變為reject狀態,也就是失敗

並且這兩個形參執行之後都可以傳入引數,我們繼續完善程式碼 我們將這兩個形參的函式封裝在建構函式內部

// 這裡我們建立了一個建構函式 引數就是執行器
function Promise(exector) {
    // 這裡我們將value 成功時候的值 reason失敗時候的值放入屬性中
    let self = this;
    this.value = undefined;
    this.reason = undefined;
    
    // 成功執行
    function resolve(value) {
      self.value = value;
    }
    
    // 失敗執行
    function reject(reason) {
        self.reason = reason;
    }
    
    exector(resolve, reject);
}
複製程式碼

這裡問題來了,我們知道,promise的執行過程是不可逆的,resolve和rejeact之間也不能相互轉化, 這裡,我們就需要加入一個狀態,判斷當前是否在pending過程,另外我們的執行器可能直接報錯,這裡我們也需要處理一下.

// 這裡我們建立了一個建構函式 引數就是執行器
function Promise(exector) {
    // 這裡我們將value 成功時候的值 reason失敗時候的值放入屬性中
    let self = this;
    // 這裡我們加入一個狀態標識
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    
    // 成功執行
    function resolve(value) {
        // 判斷是否處於pending狀態
        if (self.status === 'pending') {
            self.value = value;
            // 這裡我們執行之後需要更改狀態
            self.status = 'resolved';
        }
    }
    
    // 失敗執行
    function reject(reason) {
        // 判斷是否處於pending狀態
        if (self.status === 'pending') {
            self.reason = reason;
            // 這裡我們執行之後需要更改狀態
            self.status = 'rejected';
        }
    }
    
    // 這裡對異常進行處理
     try {
        exector(resolve, reject);
    } catch(e) {
        reject(e)
    }
}
複製程式碼

這裡先留個小坑,一會我們回頭來補上

好了,Promise基本就是這樣,是不是很簡單,這裡我們先實現一個簡易版,後面的功能會逐步新增進去,不要心急,繼續往後看

第二步 實現鏈式呼叫

new Promise之後我們怎麼去改變promise物件的狀態呢, 通過前面原生的用法我們瞭解到,需要使用then方法, then方法分別指定了resolved狀態和rejeacted狀態的回撥函式 那怎麼知道使用哪個回撥函式呢,我們剛不是在建構函式內部定義了status麼,這裡就用上啦,上程式碼

// 我們將then方法新增到建構函式的原型上 引數分別為成功和失敗的回撥

Promise.prototype.then = function(onFulfilled, onRejected) {
    // 獲取下this
    let self = this;
    if (this.status === 'resolved') {
        onFulfilled(self.value);
    }
    
    if (this.status === 'rejected') {
        onRejected(self.reason);
    }
}

複製程式碼

ok,我們現在可以自己執行試試

let promise = new Promise((resolve, reject) => {
     resolve("haha");
})


promise.then(data => {
    console.log(data); //輸出 haha
}, err=> {
    console.log(err);
})

// 多次呼叫
promise.then(data => {
    console.log(data); //輸出 haha
}, err=> {
    console.log(err);
})

複製程式碼

上面可以注意到, new Promise中的改變狀態操作我們使用的是同步,那如果是非同步呢,我們平時遇到的基本都是非同步操作,該如何解決?

這裡我們需要在建構函式中存放兩個陣列,分別儲存成功回撥和失敗的回撥 因為可以then多次,所以需要將這些函式放在陣列中 程式碼如下:

// 這裡我們建立了一個建構函式 引數就是執行器
function Promise(exector) {
    // 這裡我們將value 成功時候的值 reason失敗時候的值放入屬性中
    let self = this;
    // 這裡我們加入一個狀態標識
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    // 儲存then中成功的回撥函式
    this.onResolvedCallbacks = [];
    // 儲存then中失敗的回撥函式
    this.onRejectedCallbacks = [];
    
    // 成功執行
    function resolve(value) {
        // 判斷是否處於pending狀態
        if (self.status === 'pending') {
            self.value = value;
            // 這裡我們執行之後需要更改狀態
            self.status = 'resolved';
            // 成功之後遍歷then中成功的所有回撥函式
            self.onResolvedCallbacks.forEach(fn => fn());
        }
    }
    
    // 失敗執行
    function reject(reason) {
        // 判斷是否處於pending狀態
        if (self.status === 'pending') {
            self.reason = reason;
            // 這裡我們執行之後需要更改狀態
            self.status = 'rejected';
            // 成功之後遍歷then中失敗的所有回撥函式
            self.onRejectedCallbacks.forEach(fn => fn());
        }
    }
    
    // 這裡對異常進行處理
     try {
        exector(resolve, reject);
    } catch(e) {
        reject(e)
    }
}


// then 改造

Promise.prototype.then = function(onFulfilled, onRejected) {
    // 獲取下this
    let self = this;
    if (this.status === 'resolved') {
        onFulfulled(self.value);
    }
    
    if (this.status === 'rejected') {
        onRejected(self.reason);
    }
    
    // 如果非同步執行則位pending狀態
    if(this.status === 'pending') {
        // 儲存回撥函式
        this.onResolvedCallbacks.push(() => {
            onFulfilled(self.value);
        })

        this.onRejectedCallbacks.push(() => {
            onRejected(self.reason)
        });
    }
}


// 這裡我們可以再次實驗

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        if(Math.random() > 0.5) {
            resolve('成功');
        } else {
            reject('失敗');
        }
    })
})

promise.then((data) => {
    console.log('success' + data);
}, (err) => {
    console.log('err' + err);
})

複製程式碼

如何實現then的鏈式呼叫

這裡要開始重點了,千萬不要錯過,通過以上程式碼,我們實現了一個簡易版的promise,說簡易版是因為我們的then方法只能呼叫一次,並沒有實現原生promise中的鏈式呼叫。

那鏈式呼叫是如何實現的呢?

這裡我們需要回顧下promiseA+規範,通過檢視規範和阮一峰的es6講解可以瞭解到

then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

這裡我們看一段原生promise程式碼

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

複製程式碼

上面的程式碼使用then方法,依次指定了兩個回撥函式。 第一個回撥函式完成以後,會將返回結果作為引數,傳入第二個回撥函式。

採用鏈式的then,可以指定一組按照次序呼叫的回撥函式。這時,前一個回撥函式,有可能返回的還是一個Promise物件(即有非同步操作),這時後一個回撥函式,就會等待該Promise物件的狀態發生變化,才會被呼叫。

另外通過原生的promise我們還可以發現,上一次的成功或者失敗在返回值是一個普通型別資料的時候,都走向了下一次then的成功回撥,我們可以繼續改造then方法

Promise.prototype.then = function(onFulfilled, onRejected) {
    // 獲取下this
    let self = this;
    // 因為then方法返回的是一個promise,這裡我們新建一個promise
    let promise2 = new Promise((resolve, reject) => {
        if (this.status === 'resolved') {
            //獲取回撥的返回值
            try {
                // 當執行成功回撥的時候 可能會出現異常,那就用這個異常作為promise2的錯誤的結果
                let x = onFulfilled(self.value);
                //執行完當前成功回撥後返回結果可能是promise
                resolvePromise(promise2,x,resolve,reject);
            } catch (e) {
                reject(e);
            }
        }
    
        if (this.status === 'rejected') {
             //獲取回撥的返回值
             try {
                let x = onRejected(self.reason);
                resolvePromise(promise2,x,resolve,reject);
            } catch (e) {
                reject(e);
            }
        }
        
        // 如果非同步執行則位pending狀態
        if(this.status === 'pending') {
            // 儲存回撥函式
            this.onResolvedCallbacks.push(() => {
                 //獲取回撥的返回值
                 try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }

            })

            this.onRejectedCallbacks.push(() => {
                 //獲取回撥的返回值
                 try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            });
        }
    })
    
    return promise2;
}

複製程式碼

這裡我們看下新的then函式有什麼變化,我們一步一步分析,首先,新建了一個promise並返回,這裡是根據原生promise文件得知: then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

這裡理解之後我們繼續看,內部我們又獲取了本次then方法成功或者失敗回撥之後的返回值,賦值給變數x,這裡就會出現幾種情況,變數x可能為普通值,也可能為一個promise

我們定義了一個resolvePromise函式,將then返回的promise, 本次成功或者失敗的返回值,已經then返回promise的兩個引數傳輸這個函式中,進行一些判斷,具體實現如下:

function resolvePromise(promise2,x,resolve,reject){
    // promise2和函式執行後返回的結果是同一個物件
    
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'));
    }
    // x可能是一個promise 或者是一個普通值
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
        try{
            let then = x.then; 
            // 取物件上的屬性 怎麼能報異常呢?(這個promise不一定是自己寫的 可能是別人寫的 有的人會亂寫)
            // x可能還是一個promise 那麼就讓這個promise執行即可
            // {then:{}}
            // 這裡的邏輯不單單是自己的 還有別人的 別人的promise 可能既會呼叫成功 也會呼叫失敗
            if(typeof then === 'function'){
                then.call(x,y=>{ // 返回promise後的成功結果
                    // 遞迴直到解析成普通值為止
                    // 遞迴 可能成功後的結果是一個promise 那就要迴圈的去解析
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ // promise的失敗結果
                    reject(err);
                });
            }else{
                resolve(x);
            }
        }catch(e){
            reject(e);
        }
    }else{ // 如果x是一個常量
        resolve(x);
    }
}
複製程式碼

手寫一個自己的Promise

看的這裡是不是有點蒙圈,沒關係,我們繼續分析這個實現。

then返回一個promise ?

首選進入函式內部,我們判斷promise2是不是等於x, 這個相當於判斷上次then的返回值是不是成功和回撥的返回值,這樣就是陷入死迴圈,舉個例子:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello');
    })
})

let p2 = p.then(data => {
    return p2;
})
複製程式碼

這種寫法就會陷入一個死迴圈 所以要避免這種情況發生。 好的,繼續往下看,剛才說到x可能是一個普通值,也可能是一個promise,所以函式內部就要做一個判斷,是否是一個promise, 如果返回的是一個promise,那麼需要繼續執行這個promise,這裡用了遞迴。 平時使用promise時候我們也會注意到,各種promise庫可能會混用,所以內部對這個then的型別進行了判斷。

Ok,到這裡是不是理解了一些了,我們繼續往下看,我們知道同一個Promise內部的狀態是無法相互轉化的,這裡需要在內部做一個判斷。

function resolvePromise(promise2,x,resolve,reject){
    // promise2和函式執行後返回的結果是同一個物件
    
    if(promise2 === x){
        return reject(new TypeError('Chaining cycle'));
    }
    let called;
    // x可能是一個promise 或者是一個普通值
    if(x!==null && (typeof x=== 'object' || typeof x === 'function')){
        try{
            let then = x.then; // 取物件上的屬性 怎麼能報異常呢?(這個promise不一定是自己寫的 可能是別人寫的 有的人會亂寫)
            // x可能還是一個promise 那麼就讓這個promise執行即可
            // {then:{}}
            // 這裡的邏輯不單單是自己的 還有別人的 別人的promise 可能既會呼叫成功 也會呼叫失敗
            if(typeof then === 'function'){
                then.call(x,y=>{ // 返回promise後的成功結果
                    // 遞迴直到解析成普通值為止
                    if(called) return; // 防止多次呼叫
                    called = true;
                    // 遞迴 可能成功後的結果是一個promise 那就要迴圈的去解析
                    resolvePromise(promise2,y,resolve,reject);
                },err=>{ // promise的失敗結果
                    if(called) return;
                    called = true;
                    reject(err);
                });
            }else{
                resolve(x);
            }
        }catch(e){
            if(called) return;
            called = true;
            reject(e);
        }
    }else{ // 如果x是一個常量
        resolve(x);
    }
}
複製程式碼

我們加入一個called變數,防止互相轉化。 程式碼寫到這裡是不是就完了? 當然沒有,細心的同學會發現,原生promise還有一個用法

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello');
    })
})


p.then().then(data => {
    console.log(data);
    throw new Error('e');
}).then().then(null, err => {
    console.log(err);
})
複製程式碼

這種用法會發生值穿透,當上一個then函式沒有呼叫成功和失敗回撥的時候,值會傳遞進下一次then呼叫。

這個怎麼實現的呢,其實很簡單,我們只需要判斷每次then呼叫的時候是否傳入了成功或者失敗的回撥,沒有回撥,就繼續返回上輪then成功或者失敗傳入的值。 我們還了解到,then方法的回撥都是非同步執行的,這裡我們簡單用定時器模仿下,當然內部實現可不是這麼簡單。這裡僅作為簡單實現

程式碼如下

Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function'?onFulfilled:val=>val;
    onRejected = typeof onRejected === 'function'?onRejected: err=>{throw err}
    let self = this;
    let promise2;
    promise2 = new Promise((resolve, reject) => {
        if (self.status === 'resolved') {
            setTimeout(()=>{
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'rejected') {
            setTimeout(()=>{
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2,x,resolve,reject);
                } catch (e) {
                    reject(e);
                }
            },0)
        }
        if (self.status === 'pending') {
            self.onResolvedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(()=>{
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2,x,resolve,reject);
                    } catch (e) {
                        reject(e);
                    }
                },0)
            });
        }
    });
    return promise2
}
複製程式碼

大工告成。

image
等等,是不是少點什麼,你是不是在逗我,少俠莫急。且繼續往下看。 我們平時使用當然還有promise的一些其他方法,如catch all race。 而且還能這麼寫

Promise.resove().then()
Promise.reject().then()

複製程式碼

Promise的補充

我們一個一個來實現,就先看上面直接在Promise類上呼叫成功和失敗 我們可以這麼寫

Promise.reject = function(reason){
    return new Promise((resolve,reject)=>{
        reject(reason);
    })
}
Promise.resolve = function(value){
    return new Promise((resolve,reject)=>{
        resolve(value);
    })
}
複製程式碼

catch呢, 相當於直接走入下一次then的失敗回撥

Promise.prototype.catch = function(onRejected){
    // 預設不寫成功
    return this.then(null,onRejected);
};
複製程式碼

Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。 具體用法可以參考es6文件,這就不具體再說用法

// 傳入一個promise陣列
Promise.all = function(promises){
// 返回執行後的結果
    return new Promise((resolve,reject)=>{
        let arr = [];
        let i = 0;
        function processData(index,data){
            arr[index] = data;
            // 判斷是否全部成功
            if(++i == promises.length){
                resolve(arr);
            }
        }
        for(let i = 0;i<promises.length;i++){
            promises[i].then(data=>{ // data是成功的結果
                //將每次執行成功後的結果傳入函式
                processData(i,data);
            },reject);
        }
    })
}
複製程式碼

race就更簡單了。

Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i = 0;i<promises.length;i++){
            promises[i].then(resolve,reject);
        }
    })
}
複製程式碼

這裡我們就已經實現了promise常見的一些功能,這裡需要多看幾遍加深記憶。

手寫一個自己的Promise

相關文章