純手寫Promise,由淺入深

Yxaw發表於2019-09-26

在我的上一篇文章裡著重介紹了async的相關知識,對promise的提及甚少,現在很多面試也都要求我們有手動造輪子的能力,所以本篇文章我會以手動實現一個promise的方式來發掘一下Promise的特點.

簡單版Promise

首先我們應該知道Promise是通過建構函式的方式來建立的(new Promise( executor )),並且為 executor函式 傳遞引數:

function Promi(executor) {
  executor(resolve, reject);
  function resolve() {}
  function reject() {}
}

再來說一下Promise的三種狀態: pending-等待, resolve-成功, reject-失敗, 其中最開始為pending狀態, 並且一旦成功或者失敗, Promise的狀態便不會再改變,所以根據這點:

function Promi(executor) {
  let _this = this;
  _this.$$status = 'pending';
  executor(resolve.bind(this), reject.bind(this));
  function resolve() {
    if (_this.$$status === 'pending') {
      _this.$$status = 'full'
    }
  }
  function reject() {
    if (_this.$$status === 'pending') {
      _this.$$status = 'fail'
    }
  }
}

其中$$status來記錄Promise的狀態,只有當promise的狀態未pending時我們才會改變它的狀態為'full'或者'fail', 因為我們在兩個status函式中使用了this,顯然使用的是Promise的一些屬性,所以我們要繫結resolve與reject中的this為當前建立的Promise;
這樣我們最最最基礎的Promise就完成了(只有頭部沒有四肢...)

Promise高階 --> .then

接著,所有的Promise例項都可以用.then方法,其中.then的兩個引數,成功的回撥和失敗的回撥也就是我們所說的resolve和reject:

function Promi(executor) {
    let _this = this;
  _this.$$status = 'pending';
  _this.failCallBack = undefined;
  _this.successCallback = undefined;
  _this.error = undefined;
  executor(resolve.bind(_this), reject.bind(_this));
  function resolve(params) {
    if (_this.$$status === 'pending') {
      _this.$$status = 'success'
      _this.successCallback(params)
    }
  }
  function reject(params) {
    if (_this.$$status === 'pending') {
      _this.$$status = 'fail'
      _this.failCallBack(params)
    }
  }
}

Promi.prototype.then = function(full, fail) {
  this.successCallback = full
  this.failCallBack = fail
};

// 測試程式碼
new Promi(function(res, rej) {
  setTimeout(_ => res('成功'), 30)
}).then(res => console.log(res))

講一下這裡:
可以看到我們增加了failCallBack和successCallback,用來儲存我們在then中回撥,剛才也說過,then中可傳遞一個成功和一個失敗的回撥,當P的狀態變為resolve時執行成功回撥,當P的狀態變為reject或者出錯時則執行失敗的回撥,但是具體執行結果的控制權沒有在這裡。但是我們知道一定會呼叫其中的一個。

executor任務成功了肯定有成功後的結果,失敗了我們肯定也拿到失敗的原因。所以我們可以通過params來傳遞這個結果或者error reason(當然這裡的params也可以拆開賦給Promise例項)其實寫到這裡如果是面試題,基本上是通過了,也不會有人讓你去完整地去實現

error:用來儲存,傳遞reject資訊以及錯誤資訊

Promise進階

我想我們最迷戀的應該就是Promise的鏈式呼叫吧,因為它的出現最最最大的意義就是使我們的callback看起來不那麼hell(因為我之前講到了async比它更直接),那麼為什麼then能鏈式呼叫呢? then一定返回了一個也具有then方法的物件
我想大家應該都能猜到.then返回的也一定是一個promise,那麼這裡會有一個有趣的問題,就是.then中返回的到底是一個新promise的還是鏈式頭部的呼叫者?????

從程式碼上乍一看, Promise.then(...).catch(...) 像是針對最初的 Promise 物件進行了一連串的方法鏈呼叫。

然而實際上不管是 then 還是 catch 方法呼叫,都返回了一個新的promise物件。簡單有力地證明一下

var beginPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = beginPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(beginPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

顯而易見promise返回的是一個新的而非呼叫者
不過這樣的話難度就來了,我們看下面程式碼:

function begin() {
    return new Promise(resolve => {
      setTimeout(_ => resolve('first') , 2000)
    })
}

begin().then(data => {
  console.log(data)
  return new Promise(resolve => {

  })
}).then(res => {
  console.log(res)
});    

我們知道最後的then中函式引數永遠都不會執行,為什麼說它難呢,想一下,之所以能鏈式呼叫是因為.then()執行之後返回了一個新的promise,一定注意,我說的新的promise是then()所返回而不是data => return new Promise....(這只是then的一個引數),這樣問題就來了,我們從剛才的情況看,知道只有第一個.then中的狀態改變時第二個then中的函式引數才會執行,放到程式上說也就是需要第一個.then中返回的promise狀態改變!即:

begin().then(data => {
  console.log(data)
  return new Promise(resolve => {
      setTimeout(_ => resolve('two'), 1000)
  })
}).then(res => {
  console.log(res)
});       

直接從程式碼的角度上講,呼叫了第一個.then中的函式引數中的resolve之後第一個.then()返回的promise狀態也改變了,這句話有些繞,我用一張圖來講:
圖片描述

那麼問題就來了,我們如何使得P2的狀態發生改變通知P1?
其實這裡用觀察者模式是可以的,但是代價有點大,換個角度想,其實我們直接讓P2中的resolve等於P1中的resolve不就可以了?這樣P2中呼叫了resolve之後同步的P1也相當於onresolve了,上程式碼:

function Promi(executor) {
    let _this = this;
    _this.$$status = 'pending';
    _this.failCallBack = undefined;
    _this.successCallback = undefined;
    _this.result = undefined;
    _this.error = undefined;
    setTimeout(_ => {
        executor(_this.resolve.bind(_this), _this.reject.bind(_this));
    })
}

Promi.prototype.then = function(full, fail) {
    let newPromi = new Promi(_ => {});
    this.successCallback = full;
    this.failCallBack = fail;
    this.successDefer = newPromi.resolve.bind(newPromi);
    this.failDefer = newPromi.reject.bind(newPromi);
    return newPromi
};

Promi.prototype.resolve = function(params) {
    let _this = this;
    if (_this.$$status === 'pending') {
        _this.$$status = 'success';
        if (!_this.successCallback) return;
        let result = _this.successCallback(params);
        if (result && result instanceof Promi) {
            result.then(_this.successDefer, _this.failDefer);
            return ''
        }
        _this.successDefer(result)
    }
}

Promi.prototype.reject = function(params) {
    let _this = this;
    if (_this.$$status === 'pending') {
        _this.$$status = 'fail';
        if (!_this.failCallBack) return;
        let result = _this.failCallBack(params);
        if (result && result instanceof Promi) {
            result.then(_this.successDefer, _this.failDefer);
            return ''
        }
        _this.successDefer(result)
    }
}

// 測試程式碼
new Promi(function(res, rej) {
    setTimeout(_ => res('成功'), 500)
}).then(res => {
    console.log(res);
    return '第一個.then成功'
}).then(res => {
    console.log(res);
    return new Promi(function(resolve) {
        setTimeout(_ => resolve('第二個.then成功'), 500)
    })
}).then(res => {
    console.log(res)
    return new Promi(function(resolve, reject) {
        setTimeout(_ => reject('第三個失敗'), 1000)
    })
}).then(res => {res
    console.log(res)
}, rej => console.log(rej));

Promise完善

其實做到這裡我們還有好多好多沒有完成,比如錯誤處理,reject處理,catch實現,.all實現,.race實現,其實原理也都差不多,(all和race以及resolve和reject其實返回的都是一個新的Promise),錯誤的傳遞?還有很多細節我們都沒有考慮到,我這裡寫了一個還算是比較完善的:

function Promi(executor) {
    let _this = this;
    _this.$$status = 'pending';
    _this.failCallBack = undefined;
    _this.successCallback = undefined;
    _this.error = undefined;
    setTimeout(_ => {
        try {
            executor(_this.onResolve.bind(_this), _this.onReject.bind(_this))
        } catch (e) {
            _this.error = e;
            if (_this.callBackDefer && _this.callBackDefer.fail) {
                _this.callBackDefer.fail(e)
            } else if (_this._catch) {
                _this._catch(e)
            } else {
                throw new Error('un catch')
            }
        }
    })
}

Promi.prototype = {
    constructor: Promi,
    onResolve: function(params) {
        if (this.$$status === 'pending') {
            this.$$status = 'success';
            this.resolve(params)
        }
    },
    resolve: function(params) {
        let _this = this;
        let successCallback = _this.successCallback;
        if (successCallback) {
            _this.defer(successCallback.bind(_this, params));
        }
    },
    defer: function(callBack) {
        let _this = this;
        let result;
        let defer = _this.callBackDefer.success;
        if (_this.$$status === 'fail' && !_this.catchErrorFunc) {
            defer = _this.callBackDefer.fail;
        }
        try {
            result = callBack();
        } catch (e) {
            result = e;
            defer = _this.callBackDefer.fail;
        }
        if (result && result instanceof Promi) {
            result.then(_this.callBackDefer.success, _this.callBackDefer.fail);
            return '';
        }
        defer(result)
    },
    onReject: function(error) {
        if (this.$$status === 'pending') {
            this.$$status = 'fail';
            this.reject(error)
        }
    },
    reject: function(error) {
        let _this = this;
        _this.error = error;
        let failCallBack = _this.failCallBack;
        let _catch = _this._catch;
        if (failCallBack) {
            _this.defer(failCallBack.bind(_this, error));
        } else if (_catch) {
            _catch(error)
        } else {
            setTimeout(_ => { throw new Error('un catch promise') }, 0)
        }
    },
    then: function(success = () => {}, fail) {
        let _this = this;
        let resetFail = e => e;
        if (fail) {
            resetFail = fail;
            _this.catchErrorFunc = true;
        }
        let newPromise = new Promi(_ => {});
        _this.callBackDefer = {
            success: newPromise.onResolve.bind(newPromise),
            fail: newPromise.onReject.bind(newPromise)
        };
        _this.successCallback = success;
        _this.failCallBack = resetFail;
        return newPromise
    },
    catch: function(catchCallBack = () => {}) {
        this._catch = catchCallBack
    }
};   


// 測試程式碼

task()
    .then(res => {
        console.log('1:' + res)
        return '第一個then'
    })
    .then(res => {
        return new Promi(res => {
            setTimeout(_ => res('第二個then'), 3000)
        })
    }).then(res => {
        console.log(res)
   })
    .then(res => {
        return new Promi((suc, fail) => {
            setTimeout(_ => {
                fail('then失敗')
           }, 400)
        })
    })
    .then(res => {
        console.log(iko)
   })
    .then(_ => {}, () => {
       return new Promi(function(res, rej) {
           setTimeout(_ => rej('promise reject'), 3000)
       })
   })
    .then()
    .then()
    .then(_ => {},
        rej => {
            console.log(rej);
            return rej + '處理完成'
        })
    .then(res => {
        console.log(res);
        // 故意出錯
        console.log(ppppppp)
    })
    .then(res => {}, rej => {
        console.log(rej);
        // 再次拋錯
        console.log(oooooo)
    }).catch(e => {
        console.log(e)
   })
   

還有一段程式碼是我將所有的.then全部返回撥用者來實現的,即全程都用一個promise來記錄狀態儲存任務佇列,這裡就不發出來了,有興趣可以一起探討下.
有時間會再完善一下all, race, resolve....不過到時候程式碼結構肯定會改變,實在沒啥時間,所以講究看一下吧,歡迎交流

相關文章