手寫Promise,完美實現Promise/A+規範

寫這篇的文章的緣由是在公司內部的前端小組裏面分享了一下關於Promise的實現。感受內容還不錯,因此在這裏分享給你們。源碼文件會放到Github上面,感興趣的同窗能夠去查看源碼。javascript

什麼是Promise

Promise的核心思想是Promise表示異步操做的結果。一個Promise處於如下三種狀態之一:前端

  • pending - Promise 的初始化狀態
  • fulfilled - 表示 Promise 成功操做的狀態
  • rejected - 表示 Promise 錯誤操做的狀態

Promise 的內部狀態改變如圖所示:java

image.png

Promise的出現解決了什麼問題

  • 1.嵌套地獄的問題

Promise沒有出現以前,咱們會看到不少相似的代碼。git

const fs = require('fs')

fs.readFile('./data.txt','utf8',function(err,data){
  fs.readFile(data, 'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data);
    })
  })
})
複製代碼

Promise出現以後,就能夠採用鏈式調用的形式來寫。github

const fs = require('fs')

const readFile = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, 'utf8', (err, data) => {
      if (err) reject(err);
      resolve(data);
    })
  })
}

readFile('./data.txt').then((data) => {
  return readFile(data) 
}).then((data) => {
  return readFile(data)  
}).then((data) => {
    console.log(data);
})
複製代碼

使用了Promise以後代碼風格變得優雅了不少,寫法上也更加直觀。typescript

  • 2.處理多個異步請求併發

Promise.all的出現讓咱們能夠更加方便的處理多個任務完成時在進行處理的邏輯。json

根據 Promise/A+ 規範實現 Promise

基本功能實現

1.在動手寫代碼以前先了解一下須要實現哪些功能。promise

  • Promise constructor

new Promise 時,構造函數須要傳入一個executor() 執行器,executor函數會當即執行,而且它支持傳入兩個參數,分別是 resolverejectmarkdown

class Promise<T> {
   constructor(executor: (resolve: (value: T ) => void, reject: >(reason?: any) => void) => void){
   }
}
複製代碼
  • Promise 狀態 「Promise/A+ 2.1」

Promise 必須處於如下三種狀態之一:併發

pending(等待中),能夠轉換爲 fulfilled(完成)或 rejected(拒絕)。

當狀態從 pending 切換到 fulfilled 時,該狀態不得再過渡到其它狀態,而且必須具備一個值,該值不能更改。

當狀態從 pending 切換到 rejected 時,該狀態不得再過渡到其它狀態,而且必須有一個失敗的緣由,不能更改。

  • Promise then 方法 「Promise/A+ 2.2」

Promise 必須有一個 then 方法,then 接收兩個參數,分別是成功時的回調 onFulfilled, 和失敗時的回調 onRejected

onFulfilledonRejected 是可選的參數,而且若是傳入的 onFulfilledonRejected 不是函數的話,則必須將其忽略。

若是 onfulfilled 是一個函數。則它必須在 Promise 的狀態變成 fulfilled(完成)時才能調用,Promise 的值是傳進它的第一個參數。而且它只能被調用一次。

若是 onRejected 是一個函數,則它必須在 Promise 的狀態爲 rejected(失敗)時調用,並把失敗的緣由傳入它的第一個參數。只能被調用一次。

既然知道了須要實現那些功能,那就來動手操做一下,代碼以下:

// 使用枚舉定義Promise的狀態
enum PROMISE_STATUS {
    PENDING,
    FULFILLED,
    REJECTED
}

class _Promise<T> {
    // 保存當前狀態
    private status = PROMISE_STATUS.PENDING
    // 保存resolve的值,或者reject的緣由
    private value: T
    constructor(executor: (resolve: (value: T) => void, reject: (reason: any) => void) => void) {
        executor(this._resolve, this._reject)
    }
    
    // 根據規範完成簡易功能的then方法
    then(onfulfilled: (value: T) => any, onrejected: (value: any) => any) {
        // 2.2.1
        onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : null;
        onrejected = typeof onrejected === 'function' ? onrejected : null;

        if (this.status === PROMISE_STATUS.FULFILLED) {
            // 狀態爲fulfilled時調用成功的回調函數
            onfulfilled(this.value)
        }

        if (this.status === PROMISE_STATUS.REJECTED) {
            // 狀態爲rejected時調用失敗的回調函數
            onrejected(this.value)
        }

    }
    
    // 傳入executor方法的第一個參數,調用此方法就是成功
    private _resolve = (value) => {
        if (value === this) {
            throw new TypeError('A promise cannot be resolved with itself.');
        }
        // 只有是pending狀態才能夠更新狀態,防止二次調用
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.FULFILLED;
        this.value = value;
    }
    
    // 傳入executor方法的第二個參數,調用此方法就是失敗
    private _reject = (value) => {
        // 只有是pending狀態才能夠更新狀態,防止二次調用
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.REJECTED;
        this.value = value
    }

}
複製代碼

代碼寫完了咱們測試一下功能:

const p1 = new _Promise((resolve, reject) => {
  resolve(2)
})

p1.then(res => {
  console.log(res, 'then ok1')
})

const p2 = new _Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1000);
})

p2.then(res => {
  console.log(res, 'then ok2')
})
複製代碼

控制檯會打印出:

2 "then ok1"
複製代碼

不錯,如今已是稍見雛形。

支持異步操做

咱們已經實現了一個入門級的 Promise,可是細心的同窗應該已經發現了,then ok2 這個值沒有打印出來。

致使這個問題出現的緣由是什麼呢?原來是咱們在執行then函數的時候,因爲是異步操做,狀態一直處於pending的狀態,傳進來的回調函數沒有觸發執行。

知道了問題就好解決了。只須要把傳進來的回調函數存儲起來。在調用resolve或reject方法的時候執行就能夠了,咱們優化一下代碼:

enum PROMISE_STATUS {
    PENDING,
    FULFILLED,
    REJECTED
}

class _Promise<T> {
    private status = PROMISE_STATUS.PENDING
    private value: T
    // 保存then方法傳入的回調函數
    private callbacks = []
    constructor(executor: (resolve: (value: T) => void, reject: (reason: any) => void) => void) {
        executor(this._resolve, this._reject)
    }

    then(onfulfilled: (value: T) => any, onrejected: (value: any) => any) {
        // 2.2.1
        onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : null;
        onrejected = typeof onrejected === 'function' ? onrejected : null;

        // 把then方法傳入的回調函數整合一下
        const handle = () => {
            if (this.status === PROMISE_STATUS.FULFILLED) {
                onfulfilled && onfulfilled(this.value)
            }

            if (this.status === PROMISE_STATUS.REJECTED) {
                onrejected && onrejected(this.value)
            }
        }

        if (this.status === PROMISE_STATUS.PENDING) {
            // 當狀態是pending時,把回調函數保存進callback裏面
            this.callbacks.push(handle)
        }

        handle()
    }

    private _resolve = (value) => {
        if (value === this) {
            throw new TypeError('A promise cannot be resolved with itself.');
        }
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.FULFILLED;
        this.value = value;
        // 遍歷執行回調
        this.callbacks.forEach(fn => fn())
    }

    private _reject = (value) => {
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.REJECTED;
        this.value = value
        // 遍歷執行回調
        this.callbacks.forEach(fn => fn())
    }


}

複製代碼

在來測試一下上面的代碼:

const p2 = new _Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 1000);
})

p2.then(res => {
  console.log(res, 'then ok2')
})
複製代碼

在等待1s後,控制檯會打印出:

2 "then ok2"
複製代碼

目前已經能夠支持異步操做了。如今的你已是江湖中的高手了。

then 方法的鏈式調用

在文章一開頭介紹Promise時,提到了鏈式調用的概念.then().then(),如今就要實現這個相當重要的功能,在開始前先看一下Promise/A+的規範

then必須返回一個Promise 「Promise/A+ 2.2.7」

promise2 = promise1.then(onFulfilled, onRejected);

若是一個 onFulfilledonRejected 返回一個值 x ,則運行 Promise Resolution Procedure (會在下面實現這個方法)。

若是任何一個 onFulfilled onRejected 引起異常 epromise2 必須以 e 爲其理由 reject (拒絕).

若是 onFulfilled 不是函數且promise1狀態已經fuifilled(完成),則 promise2 必須使用與相同的值來實現 promise1

若是onRejected不是函數而 promise1 狀態爲rejected(拒絕),則 promise2 必須以與相同的理由將其拒絕 promise1

Promise Resolution Procedure 實現

首先該方法的使用方式相似於下面這種形式 resolvePromise(promise,x,...)

若是 promisex 引用相同的對象,promise 則應該以TypeError爲理由拒絕。「Promise/A+ 2.3.1」

若是 x 是一個 promise ,則應該採用它本來的狀態返回。「Promise/A+ 2.3.2」

不然,判斷 x 若是是對象或者是函數。則執行如下操做,先聲明 let then = x.then ,若是出現異常結果 e ,則以 e 做爲 promise reject(拒絕)的緣由。若是 then 是個函數,則用 call 執行 then ,把 this 指向爲 x ,第一個參數用 resolvePromise 調用,第二個用 rejectPromise 調用「Promise/A+ 2.3.3」

若是 x 不是對象或者方法,則使用 x 的值 resolve 完成 「Promise/A+ 2.3.4」

只是經過文字不太容易理解,咱們來看一下代碼的實現:

enum PROMISE_STATUS {
    PENDING,
    FULFILLED,
    REJECTED
}

class _Promise<T> {
    private status = PROMISE_STATUS.PENDING
    private value: T
    private callbacks = []
    constructor(executor: (resolve: (value: T) => void, reject: (reason: any) => void) => void) {
        executor(this._resolve, this._reject)
    }

    then(onfulfilled: (value: T) => any, onrejected: (value: any) => any) {
        // 2.2.1
        onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : null;
        onrejected = typeof onrejected === 'function' ? onrejected : null;

        const nextPromise = new _Promise((nextResolve, nextReject) => {
            const handle = () => {
                if (this.status === PROMISE_STATUS.FULFILLED) {
                    const x = (onfulfilled && onfulfilled(this.value))
                    this._resolvePromise(nextPromise, x, nextResolve, nextReject)
                }

                if (this.status === PROMISE_STATUS.REJECTED) {
                    if (onrejected) {
                        const x = onrejected(this.value)
                        this._resolvePromise(nextPromise, x, nextResolve, nextReject)
                    } else {
                        nextReject(this.value)
                    }
                }

            }
            if (this.status === PROMISE_STATUS.PENDING) {
                this.callbacks.push(handle)
            } else {
                handle()
            }

        });
        return nextPromise

    }

    private _resolve = (value) => {
        if (value === this) {
            throw new TypeError('A promise cannot be resolved with itself.');
        }
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.FULFILLED;
        this.value = value;
        this.callbacks.forEach(fn => fn())
    }

    private _reject = (value) => {
        if (this.status !== PROMISE_STATUS.PENDING) return;
        this.status = PROMISE_STATUS.REJECTED;
        this.value = value
        this.callbacks.forEach(fn => fn())
    }

    private _resolvePromise = <T>(nextPromise: _Promise<T>, x: any, resolve, reject) => {

        // 2.3.1 nextPromise 不能和 x 相等
        if (nextPromise === x) {
            return reject(new TypeError('The promise and the return value are the same'));
        }

        // 2.3.2 若是 x 是 Promise 返回 x 的狀態和值
        if (x instanceof _Promise) {
            x.then(resolve, reject)
        }

        // 2.3.3 若是 x 是對象或者函數執行 if 裏面的邏輯
        if (typeof x === 'object' || typeof x === 'function') {
            if (x === null) {
                return resolve(x);
            }

            // 2.3.3.1
            let then;
            try {
                then = x.then;
            } catch (error) {
                return reject(error);
            }

            // 2.3.3.3
            if (typeof then === 'function') {
                // 聲明called 在調用過一次resolve或者reject以後,修改成true,保證只能調用一次
                let called = false; 
                try {
                    then.call(x, y => {
                        if (called) return; // 2.3.3.3.4.1
                        called = true;
                        // 遞歸解析的過程(由於可能 promise 中還有 promise)
                        this._resolvePromise(nextPromise, y, resolve, reject) 
                    }, r => {
                        if (called) return; // 2.3.3.3.4.1
                        called = true;
                        reject(r)
                    })
                } catch (e) {
                    if (called) return; // 2.3.3.3.4.1
                    // 2.3.3.3.4
                    reject(e)
                }
            } else {
                // 2.3.3.4
                resolve(x)
            }
        } else {
            // 2.3.4
            resolve(x);
        }

    }

}

複製代碼

目前已經實現能夠鏈式調用的功能了,咱們來測試一下:

const p3 = new _Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3)
    }, 1000);
})

p3.then(res => {
  console.log(res, 'then ok3')
  return '鏈式調用'
}).then(res => {
  console.log(res)
})
複製代碼

等待1s以後,控制檯會打印出:

3 "then ok3"
"鏈式調用"
複製代碼

支持微任務

有沒有同窗想到還缺乏了一個尤其重要的功能,那就是微任務。咱們應該如何實現和內置 Promise 同樣的微任務流程呢?

Web Api 裏面有這樣一個方法 MutationObserver。咱們能夠基於它實現微任務的功能。而且也已經有相關的庫給咱們封裝好了這個方法,它就是 asap。只要把須要以微任務執行的函數傳入便可。

asap(function () {
    // ...
});
複製代碼

其實在 Web Api 裏面還有這樣一個方法 queueMicrotask 能夠直接使用。使用方式也是把要以微任務執行的函數傳入進去便可。

self.queueMicrotask(() => {
  // 函數的內容
})
複製代碼

queueMicrotask 惟一的缺點就是兼容性不太好,在生產環境中建議仍是使用 asap 這個庫來實現微任務。

把以前寫好的 Promise then 方法稍微作一下調整:

then(onfulfilled: (value: T) => any, onrejected: (value: any) => any) {
        // 2.2.1
        onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : null;
        onrejected = typeof onrejected === 'function' ? onrejected : null;

        const nextPromise = new _Promise((nextResolve, nextReject) => {
            const _handle = () => {
                if (this.status === PROMISE_STATUS.FULFILLED) {
                    const x = (onfulfilled && onfulfilled(this.value))
                    this._resolvePromise(nextPromise, x, nextResolve, nextReject)
                }

                if (this.status === PROMISE_STATUS.REJECTED) {
                    if (onrejected) {
                        const x = onrejected(this.value)
                        this._resolvePromise(nextPromise, x, nextResolve, nextReject)
                    } else {
                        nextReject(this.value)
                    }
                }

            }
            const handle = () => {
                // 支持微任務
                queueMicrotask(_handle)
            }
            if (this.status === PROMISE_STATUS.PENDING) {
                this.callbacks.push(handle)
            } else {
                handle()
            }

        });
        return nextPromise

    }
複製代碼

如今完美支持微任務,和內置 Promises 事件執行順序一致。咱們來測試一下:

console.log('first')
const p1 = new _Promise(function (resolve) {
  console.log('second')
  resolve('third')
})
p1.then(console.log)
console.log('fourth')
複製代碼

能夠看到控制檯打印的結果爲:

first
second
fourth
third
複製代碼

到這裏,咱們已經把Promise最關鍵的功能完成了:支持異步操做then支持鏈式調用支持微任務

測試完成的 Promise 是否符合規範

1.下載Promise/A+規範提供了一個專門的測試腳本 promises-aplus-tests

yarn add promises-aplus-tests -D
複製代碼

2.在咱們的代碼中加入如下代碼:

(_Promise as any).deferred = function () {
    let dfd = {} as any;
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}

module.exports = _Promise;
複製代碼

3.修改 package.json 文件增長如下內容(./dist/promise/index.js是須要測試的文件路徑):

{
  "scripts": {
    "test": "promises-aplus-tests ./dist/promise/index.js"
  }
}
複製代碼

4.執行 yarn test

image.png

能夠看到872個測試用例所有經過。

Promise 的其它 API 實現

到目前爲止,上述代碼已經完整的按照 Promise/A+ 規範實現了,但還有一些內置Api沒有實現。下面就把這些內置的方法來實現:

class _Promise {

    catch(onrejected) {
        return this.then(null, onrejected)
    }

    finally(cb) {
        return this.then(
            value => _Promise.resolve(cb()).then(() => value),
            reason => _Promise.resolve(cb()).then(() => { throw reason })
        );
    }

    static resolve(value) {
        if (value instanceof _Promise) {
            return value;
        }

        return new _Promise(resolve => {
            resolve(value);
        });
    }

    static reject(reason) {
        return new _Promise((resolve, reject) => {
            reject(reason);
        });
    }

    static race(promises) {
        return new _Promise(function (resolve, reject) {
            if (!Array.isArray(promises)) {
                return reject(new TypeError('Promise.race accepts an array'));
            }
            for (var i = 0, len = promises.length; i < len; i++) {
                _Promise.resolve(promises[i]).then(resolve, reject);
            }
        });

    }

    static all(promises) {
        let result = [];
        let i = 0;

        return new _Promise((resolve, reject) => {
            const processValue = (index, value) => {
                result[index] = value;
                i++;
                if (i == promises.length) {
                    resolve(result);
                };
            };
            for (let index = 0; index < promises.length; index++) {
                promises[index].then(value => {
                    processValue(index, value);
                }, reject);
            };
        });
    }

    static allSettled(promises) {
        let result = []
        let i = 0;
        return new _Promise((resolve, reject) => {
            const processValue = (index, value, status: 'fulfilled' | 'rejected') => {
                result[index] = { status, value }
                i++;
                if (i == promises.length) {
                    resolve(result);
                };
            };


            for (let index = 0; index < promises.length; index++) {
                promises[index].then(value => {
                    processValue(index, value, 'fulfilled')
                }, value => {
                    processValue(index, value, 'rejected')
                });
            };
        })
    }
    
    ...
}
複製代碼

如何實現 asyncawait

咱們完成了 Promise 的實現。可是你們有沒有想過 asyncawait 這個 Promise 的語法糖如何實現呢?

這裏咱們就要藉助 Generator 函數來實現這個功能,廢話少說,直接上代碼:

let gp1 = new _Promise(r => {
    setTimeout(() => {
        r(1)
    }, 1000);
})

let gp2 = new _Promise(r => {
    setTimeout(() => {
        r(2)
    }, 1000);
})

function* gen() {
    let a = yield gp1
    let b = yield gp2
    return b + a
}

function run(gen) {
    return new _Promise(function (resolve, reject) {
        g = gen()
        function next(v) {
          ret = g.next(v)
          if (ret.done) return resolve(ret.value);
          _Promise.resolve(ret.value).then(next)
        }
        next()
    })
}

run(gen).then(console.log)
複製代碼

控制檯裏面會打印出結果爲:3

若是對這個 run 函數感興趣,推薦去看下這個 co 庫實現,代碼寫的很是簡潔,只有一百行左右,值的一看。

總結

用了近半天的時間才把這篇文章給寫出來。其中的源碼文件已經放到 Github 上面。不想手敲一遍的同窗能夠直接拉下來代碼執行查看結果。若是你有不一樣的意見或想法也歡迎留言。

相關資源連接

相關文章
相關標籤/搜索