只會用就out了,手寫一個符合規範的Promise

Promise是什麼

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。javascript

Promise是處理異步編碼的一個解決方案,在Promise出現之前,異步代碼的編寫都是經過回調函數來處理的,回調函數自己沒有任何問題,只是當屢次異步回調有邏輯關係時就會變得複雜:前端

const fs = require('fs');
fs.readFile('1.txt', (err,data) => {
    fs.readFile('2.txt', (err,data) => {
        fs.readFile('3.txt', (err,data) => {
            //可能還有後續代碼
        });
    });
});
複製代碼

上面讀取了3個文件,它們是層層遞進的關係,能夠看到多個異步代碼套在一塊兒不是縱向發展的,而是橫向,不管是從語法上仍是從排錯上都很差,因而Promise的出現能夠解決這一痛點。java

上述代碼若是改寫成Promise版是這樣:面試

const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);

readFile('1.txt')
    .then(data => {
        return readFile('2.txt');
    }).then(data => {
        return readFile('3.txt');
    }).then(data => {
        //...
    });
複製代碼

能夠看到,代碼是從上至下縱向發展了,更加符合人們的邏輯。數組

下面手寫一個Promise,按照Promises/A+規範,能夠參照規範原文: Promises/A+規範promise

手寫實現Promise是一道前端經典的面試題,好比美團的面試就是必考題,Promise的邏輯仍是比較複雜的,考慮的邏輯也比較多,下面總結手寫Promise的關鍵點,和怎樣使用代碼來實現它。異步

Promise代碼基本結構

實例化Promise對象時傳入一個函數做爲執行器,有兩個參數(resolve和reject)分別將結果變爲成功態和失敗態。咱們能夠寫出基本結構函數

function Promise(executor) {
    this.state = 'pending'; //狀態
    this.value = undefined; //成功結果
    this.reason = undefined; //失敗緣由

    function resolve(value) {
        
    }

    function reject(reason) {

    }
}

module.exports = Promise;
複製代碼

其中state屬性保存了Promise對象的狀態,規範中指明,一個Promise對象只有三種狀態:等待態(pending)成功態(resolved)和失敗態(rejected)。 當一個Promise對象執行成功了要有一個結果,它使用value屬性保存;也有可能因爲某種緣由失敗了,這個失敗緣由放在reason屬性中保存。測試

then方法定義在原型上

每個Promise實例都有一個then方法,它用來處理異步返回的結果,它是定義在原型上的方法,咱們先寫一個空方法作好準備:ui

Promise.prototype.then = function (onFulfilled, onRejected) {
};
複製代碼

當實例化Promise時會當即執行

當咱們本身實例化一個Promise時,其執行器函數(executor)會當即執行,這是必定的:

let p = new Promise((resolve, reject) => {
    console.log('執行了');
});
複製代碼

運行結果:

執行了
複製代碼

所以,當實例化Promise時,構造函數中就要立刻調用傳入的executor函數執行

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    executor(resolve, reject); //立刻執行
    
    function resolve(value) {}
    function reject(reason) {}
}
複製代碼

已是成功態或是失敗態不可再更新狀態

規範中規定,當Promise對象已經由pending狀態改變爲了成功態(resolved)或是失敗態(rejected)就不能再次更改狀態了。所以咱們在更新狀態時要判斷,若是當前狀態是pending(等待態)纔可更新:

function resolve(value) {
        //當狀態爲pending時再作更新
        if (_this.state === 'pending') {
            _this.value = value;//保存成功結果
            _this.state = 'resolved';
        }

    }

    function reject(reason) {
    //當狀態爲pending時再作更新
        if (_this.state === 'pending') {
            _this.reason = reason;//保存失敗緣由
            _this.state = 'rejected';
        }
    }
複製代碼

以上能夠看到,在resolve和reject函數中分別加入了判斷,只有當前狀態是pending纔可進行操做,同時將成功的結果和失敗的緣由都保存到對應的屬性上。以後將state屬性置爲更新後的狀態。

then方法的基本實現

當Promise的狀態發生了改變,不管是成功或是失敗都會調用then方法,因此,then方法的實現也很簡單,根據state狀態來調用不一樣的回調函數便可:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === 'resolved') {
        //判斷參數類型,是函數執行之
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value);
        }

    }
    if (this.state === 'rejected') {
        if (typeof onRejected === 'function') {
            onRejected(this.reason);
        }
    }
};
複製代碼

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

讓Promise支持異步

代碼寫到這裏彷佛基本功能都實現了,但是還有一個很大的問題,目前此Promise還不支持異步代碼,若是Promise中封裝的是異步操做,then方法無能爲力:

let p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    },500);
});

p.then(data => console.log(data)); //沒有任何結果
複製代碼

運行以上代碼發現沒有任何結果,本意是等500毫秒後執行then方法,哪裏有問題呢?緣由是setTimeout函數使得resolve是異步執行的,有延遲,當調用then方法的時候,此時此刻的狀態仍是等待態(pending),所以then方法即沒有調用onFulfilled也沒有調用onRejected。

這個問題如何解決?咱們能夠參照發佈訂閱模式,在執行then方法時若是還在等待態(pending),就把回調函數臨時寄存到一個數組裏,當狀態發生改變時依次從數組中取出執行就行了,清楚這個思路咱們實現它,首先在類上新增兩個Array類型的數組,用於存放回調函數:

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFunc = [];//保存成功回調
    this.onRejectedFunc = [];//保存失敗回調
    //其它代碼略...
}
複製代碼

這樣當then方法執行時,若狀態還在等待態(pending),將回調函數依次放入數組中:

Promise.prototype.then = function (onFulfilled, onRejected) {
    //等待態,此時異步代碼尚未走完
    if (this.state === 'pending') {
        if (typeof onFulfilled === 'function') {
            this.onFulfilledFunc.push(onFulfilled);//保存回調
        }
        if (typeof onRejected === 'function') {
            this.onRejectedFunc.push(onRejected);//保存回調
        }
    }
    //其它代碼略...
};
複製代碼

寄存好了回調,接下來就是當狀態改變時執行就行了:

function resolve(value) {
        if (_this.state === 'pending') {
            _this.value = value;
            //依次執行成功回調
            _this.onFulfilledFunc.forEach(fn => fn(value));
            _this.state = 'resolved';
        }

    }

    function reject(reason) {
        if (_this.state === 'pending') {
            _this.reason = reason;
            //依次執行失敗回調
            _this.onRejectedFunc.forEach(fn => fn(reason));
            _this.state = 'rejected';
        }
    }
複製代碼

至此,Promise已經支持了異步操做,setTimeout延遲後也可正確執行then方法返回結果。

鏈式調用

Promise處理異步代碼最強大的地方就是支持鏈式調用,這塊也是最複雜的,咱們先梳理一下規範中是怎麼定義的:

  1. 每一個then方法都返回一個新的Promise對象(原理的核心
  2. 若是then方法中顯示地返回了一個Promise對象就以此對象爲準,返回它的結果
  3. 若是then方法中返回的是一個普通值(如Number、String等)就使用此值包裝成一個新的Promise對象返回。
  4. 若是then方法中沒有return語句,就視爲返回一個用Undefined包裝的Promise對象
  5. 若then方法中出現異常,則調用失敗態方法(reject)跳轉到下一個then的onRejected
  6. 若是then方法沒有傳入任何回調,則繼續向下傳遞(值的傳遞特性)。

規範中說的很抽像,咱們能夠把很差理解的點使用代碼演示一下。

其中第3項,若是返回是個普通值就使用它包裝成Promise,咱們用代碼來演示:

let p =new Promise((resolve,reject)=>{
    resolve(1);
});

p.then(data=>{
    return 2; //返回一個普通值
}).then(data=>{
    console.log(data); //輸出2
});
複製代碼

可見,當then返回了一個普通的值時,下一個then的成功態回調中便可取到上一個then的返回結果,說明了上一個then正是使用2來包裝成的Promise,這符合規範中說的。

第4項,若是then方法中沒有return語句,就視爲返回一個用Undefined包裝的Promise對象

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => {
    //沒有return語句
}).then(data => {
    console.log(data); //undefined
});
複製代碼

能夠看到,當沒有返回任何值時不會報錯,沒有任何語句時實際上就是return undefined;即將undefined包裝成Promise對象傳給下一個then的成功態。

第6項,若是then方法沒有傳入任何回調,則繼續向下傳遞,這是什麼意思呢?這就是Promise中值的穿透,仍是用代碼演示一下:

let p = new Promise((resolve, reject) => {
    resolve(1);
});

p.then(data => 2)
.then()
.then()
.then(data => {
    console.log(data); //2
});
複製代碼

以上代碼,在第一個then方法以後連續調用了兩個空的then方法 ,沒有傳入任何回調函數,也沒有返回值,此時Promise會將值一直向下傳遞,直到你接收處理它,這就是所謂的值的穿透。

如今能夠明白鏈式調用的原理,不管是何種狀況then方法都會返回一個Promise對象,這樣纔會有下個then方法。

搞清楚了這些點,咱們就能夠動手實現then方法的鏈式調用,一塊兒來完善它:

Promise.prototype.then = function (onFulfilled, onRejected) {
    var promise2 = new Promise((resolve, reject) => {
    //代碼略...
    }
    return promise2;
};
複製代碼

首先,不論何種狀況then都返回Promise對象,咱們就實例化一個新promise2並返回。

接下來就處理根據上一個then方法的返回值來生成新Promise對象,因爲這塊邏輯較複雜且有不少處調用,咱們抽離出一個方法來操做,這也是規範中說明的:

/** * 解析then返回值與新Promise對象 * @param {Object} promise2 新的Promise對象 * @param {*} x 上一個then的返回值 * @param {Function} resolve promise2的resolve * @param {Function} reject promise2的reject */
function resolvePromise(promise2, x, resolve, reject) {
    //...
}
複製代碼

resolvePromise方法用來封裝鏈式調用產生的結果,下面咱們分別一個個狀況的寫出它的邏輯,首先規範中說明,若是promise2x 指向同一對象,就使用TypeError做爲緣由轉爲失敗。原文以下:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

這是什麼意思?其實就是循環引用,當then的返回值與新生成的Promise對象爲同一個(引用地址相同),則會拋出TypeError錯誤:

let promise2 = p.then(data => {
    return promise2;
});
複製代碼

運行結果:

TypeError: Chaining cycle detected for promise #<Promise>
複製代碼

很顯然,若是返回了本身的Promise對象,狀態永遠爲等待態(pending),再也沒法成爲resolved或是rejected,程序會死掉,所以首先要處理它:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise發生了循環引用'));
    }
}
複製代碼

接下來就是分各類狀況處理。當x就是一個Promise,那麼就執行它,成功即成功,失敗即失敗。若x是一個對象或是函數,再進一步處理它,不然就是一個普通值:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Promise發生了循環引用'));
    }

    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //多是個對象或是函數
    } else {
        //不然是個普通值
        resolve(x);
    }
}
複製代碼

此時規範中說明,如果個對象,則嘗試將對象上的then方法取出來,此時若是報錯,那就將promise2轉爲失敗態。原文:

If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.

function resolvePromise(promise2, x, resolve, reject) {
    //代碼略...
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //多是個對象或是函數
        try {
            let then = x.then;//取出then方法引用
        } catch (e) {
            reject(e);
        }
        
    } else {
        //不然是個普通值
        resolve(x);
    }
}
複製代碼

多說幾句,爲何取對象上的屬性有報錯的可能?Promise有不少實現(bluebird,Q等),Promises/A+只是一個規範,你們都按此規範來實現Promise纔有可能通用,所以全部出錯的可能都要考慮到,假設另外一我的實現的Promise對象使用Object.defineProperty()惡意的在取值時拋錯,咱們能夠防止代碼出現Bug。

此時,若是對象中有then,且then是函數類型,就能夠認爲是一個Promise對象,以後,使用x做爲this來調用then方法。

If then is a function, call it with x as this

//其餘代碼略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //多是個對象或是函數
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            //then是function,那麼執行Promise
            then.call(x, (y) => {
                resolve(y);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //不然是個普通值
    resolve(x);
}
複製代碼

這樣鏈式寫法就基本完成了。可是還有一種極端的狀況,若是Promise對象轉爲成功態或是失敗時傳入的仍是一個Promise對象,此時應該繼續執行,直到最後的Promise執行完。

p.then(data => {
    return new Promise((resolve,reject)=>{
        //resolve傳入的仍是Promise
        resolve(new Promise((resolve,reject)=>{
            resolve(2);
        }));
    });
})
複製代碼

此時就要使用遞歸操做了。

規範中原文以下:

If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason.

很簡單,把調用resolve改寫成遞歸執行resolvePromise方法便可,這樣直到解析Promise成一個普通值纔會終止,即完成此規範:

//其餘代碼略...
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //多是個對象或是函數
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            let y = then.call(x, (y) => {
                //遞歸調用,傳入y如果Promise對象,繼續循環
                resolvePromise(promise2, y, resolve, reject);
            }, (r) => {
                reject(r);
            });
        } else {
            resolve(x);
        }
    } catch (e) {
        reject(e);
    }

} else {
    //是個普通值,最終結束遞歸
    resolve(x);
}

複製代碼

到此,鏈式調用的代碼已所有完畢。在相應的地方調用resolvePromise方法便可。

最後的最後

其實,寫到此處Promise的真正源碼已經寫完了,可是距離100分還差一分,是什麼呢?

規範中說明,Promise的then方法是異步執行的。

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

ES6的原生Promise對象已經實現了這一點,可是咱們本身的代碼是同步執行,不相信能夠試一下,那麼如何將同步代碼變成異步執行呢?可使用setTimeout函數來模擬一下:

setTimeout(()=>{
    //此處的代碼會異步執行
},0);
複製代碼

利用此技巧,將代碼then執行處的全部地方使用setTimeout變爲異步便可,舉個栗子:

setTimeout(() => {
    try {
        let x = onFulfilled(value);
        resolvePromise(promise2, x, resolve, reject);
    } catch (e) {
        reject(e);
    }
},0);
複製代碼

好了,如今已是滿分的Promise源碼了。

滿分的測試

好不容易寫好的Promise源碼,最終是否真的符合Promises/A+規範,開源社區提供了一個包用於測試咱們的代碼:promises-aplus-tests

這個包的使用方法不在詳述,此包能夠一項項的檢查咱們寫的代碼是否合規,若是有任一項不符就會給咱們報出來,若是檢查你的代碼一路都是綠色,那恭喜,你的Proimse已經合法了,能夠上線提供給別人使用了:

872項測試經過!

如今源碼都會寫,終於能夠自信的回答面試官的問題了。

相關文章
相關標籤/搜索