前端工程師必知之Promise的實現

xuerensusu發表於2018-05-18

Promise是什麼?

在Javascript的世界中,程式碼都是單執行緒執行的。這就導致我們的程式碼裡會有巢狀回撥函式,一旦巢狀過多,導致程式碼不容易理解和維護。

前端工程師必知之Promise的實現

為了降低非同步程式設計的複雜性,開發人員一直在尋找各種解決方案,Promise只是用來處理非同步操作的其中一個方案。

下面我就結合著Promise的使用場景,來逐一手寫Promise的實現原理。

因為ES6最終採用了Promise/A+ 規範,下面所有程式碼都是基於Promise/A+規範來實現。有興趣的同學可以檢視 Promise/A+規範 https://promisesaplus.com/


Promise的基本使用:

let promise = new Promise((resolve,reject)=>{
  resolve('success'); //這裡如果是reject('fail')
});
promise.then((res)=>{
  console.log(res); // 輸出:success
},(err)=>{
  console.log(err); // 上面如果執行reject('fail') 這裡就輸出:fail
});
複製程式碼

看圖就很好理解了,resolve找then裡的成功回撥,reject找then裡失敗的回撥。

前端工程師必知之Promise的實現

那麼如何用我們自己的方式一步步實現這樣的一個Promise類呢?直接上程式碼吧,我會註釋得稍微詳細點。

class Promise { //建立一個Promise類
    constructor(executor) {
        this.status = 'pending'; //初始預設狀態為pending
        this.value = undefined; //預設賦值為undefined
        this.reason = undefined; //預設賦值為undefined
        let resolve = (value) => {
            if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
                this.value = value; //將傳遞進來的的值賦給value儲存
                this.status = 'resolved'; //將狀態設定成resolved
            }
        }
        let reject = (reason) => {
            if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
                this.reason = reason; //將傳遞進來的失敗原因賦給reason儲存
                this.status = 'rejected'; //將狀態設定成rejected
            }
        }
        executor(resolve, reject); //預設執行executor
    }
    then(onFulfilled, onRejected) { //等同於es5的Promise.prototype.then 當呼叫then的時候,根據狀態,來執行不同的函式
        if (this.status === 'resolved') { //如果狀態是resolved
            onFulfilled(this.value); //執行成功的resolve,並將成功後的值傳遞過去
        }
        if (this.status === 'rejected') { //如果狀態是rejected
            onRejected(this.reason); //執行失敗的reject,並將失敗原因傳遞過去
        }
    }
}
module.exports = Promise; //將Promise匯出

複製程式碼

現在只要在js程式碼中引入Promise庫,就能實現最基本的Promise功能了。

輸出結果:success


還有一點需要注意,就是當程式碼出現錯誤的情況下,我們需要能夠捕獲錯誤,並且處理錯誤。所以在executor(resolve, reject)這一行程式碼需要處理下,包上一層try/catch,如下:

try {
    executor(resolve, reject); 
} catch (e) {
    reject(e); //如果發生錯誤,將錯誤放入reject中
}
複製程式碼

setTimeout呼叫問題

此時的Promise類可以實現呼叫resolve,也可以呼叫reject。呼叫then之後都會執行相應的函式。看似已經沒有問題了。但如果將resolve放在一個定時器裡呢?看看下面的程式碼:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('setTimeout');
    }, 500);
})
promise.then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
})
複製程式碼

我們會發現上面的程式碼在控制檯完全沒有反應,這是為什麼呢?實際上是因為我們壓根就沒有處理這種延遲呼叫的情況。現在我們來處理以下:

class Promise { //建立一個Promise類
    constructor(executor) {
        ...此處略去部分程式碼
        this.successStore = []; //定義一個存放成功函式的陣列
        this.failStore = []; //定義一個存放失敗函式的陣列
        let resolve = (value) => {
            if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
                ...此處略去部分程式碼
                this.successStore.forEach(fnc => fnc()); //一次執行陣列中的成功函式
            }
        }
        let reject = (reason) => {
            if (this.status === 'pending') { //只有狀態為pending才能轉換狀態
                ...此處略去部分程式碼
                this.failStore.forEach(fnc => fnc()) //依次執行陣列中的失敗函式
            }
        }
       ...此處略去部分程式碼

    }
    then(onFulfilled, onRejected) { //等同於es5的Promise.prototype.then 當呼叫then的時候,根據狀態,來執行不同的函式
        ...此處略去部分程式碼
        if (this.status === 'pending') { //此處增加一種狀態判斷
            this.successStore.push(() => { //當狀態為pending時將成功的函式存放到陣列裡
                onFulfilled(this.value);
            })
            this.failStore.push(() => { //當狀態為pending時將失敗的函式存放到陣列中
                onRejected(this.reason);
            })
        }
    }
}
module.exports = Promise; //將Promise匯出

複製程式碼

這樣就解決了setTimeout呼叫的問題了。


then的鏈式呼叫

前端工程師必知之Promise的實現
細心的同學肯定看到了,在我們上面的程式碼中,其實並沒有處理then的鏈式呼叫。如果此時使用then()方法的鏈式呼叫,會報錯:TypeError: Cannot read property 'then' of undefined。

Promise/A+規範裡說了,then()方法返回的必須是Promise例項,這裡我們可以理解成返回的必須是一個新的Promise例項。所以then()方法後面可以繼續跟另一個then()方法進行鏈式呼叫。

但是then()方法裡可能發揮一個值或者返回的是一個Promise例項,這就需要我們分別處理這兩種情況。

第一種:

let promise = new Promise((resolve, reject) => {
    resolve('success');
})
p.then((res)=>{
    console.log(res); // success
    return 'hello world!' 
},(err)=>{
    console.log(err); 
}).then((res)=>{
    console.log(res); // hello world   
},(err)=>{
    console.log(err);
})
複製程式碼

在then()方法的回撥裡返回一個普通值,無論是成功還是失敗的回撥,都會進入到下一個then()的成功態裡。如下:

let promise = new Promise((resolve, reject) => {
    reject('fail');
})
p.then((res)=>{
    console.log(res); 
},(err)=>{
    console.log(err);  //fail
    return 'fail';
}).then((res)=>{
    console.log(res);  //fail 注意:此時走的是成功回撥,並非失敗的回撥
},(err)=>{
    console.log(err); 
})
複製程式碼

第二種:

let promise = new Promise((resolve, reject) => {
    resolve();
})
promise.then((res)=>{
    return new Promise((resolve,reject)=>{ //返回一個新的Promise
        resolve('hello world'); 
    })
},(err)=>{
    console.log(err); 
}).then((res)=>{
    console.log(res); //hello world    
},(err)=>{
    console.log(err);
})
複製程式碼

這兩種情況都要考慮到,我們修改下程式碼:

function handlePromise(promise2, x, resolve, reject) {
    if (promise2 === x) { //promise2是否等於x,也就是判斷是否將自己本身返回
        return reject(new TypeError('circular reference')); //如果是丟擲錯誤
    }
    //判斷x不是bull且x是物件或者函式
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        let called; //called控制resolve或reject 只執行一次,多次呼叫沒有任何作用。
        try {
            let then = x.then; //取x.then()方法
            if (typeof then === 'function') { //如果是函式,就認為它是返回新的promise
                then.call(x, y => {  //如果y是promise繼續遞迴解析
                    if (called) return;
                    called = true;
                    handlePromise(promise2, y, resolve, reject); //遞迴解析promise
                }, r => {
                    if (called) return;
                    called = true;
                    reject(r)
                })
            } else { //不是函式,就是普通物件
                resolve(x); //直接將物件返回
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else { //x是普通值,直接走then的成功回撥
        resolve(x);
    }
}
class Promise {
    constructor(executor) {
        this.status = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.successStore = [];
        this.failStore = [];
        let resolve = (value) => {
            if (this.status === 'pending') {
                this.value = value;
                this.status = 'resolved';
                this.successStore.forEach(fn => fn());
            }
        }
        let reject = (reason) => {
            if (this.status === 'pending') {
                this.reason = reason;
                this.status = 'rejected';
                this.failStore.forEach(fn => fn());
            }
        }
        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    then(onFulfilled, onRejected) { //原型上的方法
        let promise2; // 返回的新的promise
        if (this.status === 'resolved') {
            promise2 = new Promise((resolve, reject) => {
                try {
                    let x = onFulfilled(this.value);
                    handlePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }

            })
        }
        if (this.status === 'rejected') {
            promise2 = new Promise((resolve, reject) => {
                try {
                    let x = onRejected(this.reason); //x存放返回的結果
                    handlePromise(promise2, x, resolve, reject); //處理返回結果的函式,已經在上面定義
                } catch (e) {
                    reject(e);//報錯執行reject
                }
            })
        }
        if (this.status === 'pending') {
            promise2 = new Promise((resolve, reject) => {
                this.successStore.push(() => {
                    try {
                        let x = onFulfilled(this.value); //x存放返回的結果
                        handlePromise(promise2, x, resolve, reject);//處理返回結果的函式,已經在上面定義
                    } catch (e) {
                        reject(e); //報錯執行reject
                    }

                })
                this.failStore.push(() => {
                    try {
                        let x = onRejected(this.reason);//x存放返回的結果
                        handlePromise(promise2, x, resolve, reject);//處理返回結果的函式,已經在上面定義
                    } catch (e) {
                        reject(e);//報錯執行reject
                    }
                })
            })
        }
        return promise2; //返回新的promise
    }
}
module.exports = Promise;

複製程式碼

處理值穿透

先看例項

let promise = new Promise((resolve, reject)=>{
    resolve('hello world');
})
promise.then().then().then((res)=>{ 
    console.log(res);//我們希望可以正常列印出hello world,如何處理呢?
})
複製程式碼

只需要在then的原型方法上加上一層判斷,判斷then裡是否傳遞裡函式,如果沒有傳遞,我們手動傳遞一個函式,並讓值返回。

then(onFulfilled, onRejected) { //原型上的方法
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y; //判斷是否是一個函式
    onRejected = typeof onRejected === 'function' ? onRejected : errr => { //判斷是否是一個函式
        throw err; //注意,這裡不是返回值,而是丟擲錯誤
    }
}
複製程式碼

這樣當我們呼叫Promise時就可以正常列印出hello world了。

新增catch方法

下面我們繼續新增catch方法

then(onFulfilled, onRejected){
    ...
}
catch(onRejected){ //在此處新增原型上的方法catch
    return this.then(null,onRejected);
}
複製程式碼

catch()其實就是簡易版的then()方法,它沒有成功的回撥,只有失敗的回撥。所以按照上面的方式處理下就可以了。

新增Promise.all方法

Promise.all = function (promiseArrs) { //在Promise類上新增一個all方法,接受一個傳進來的promise陣列
    return new Promise((resolve, reject) => { //返回一個新的Promise
        let arr = []; //定義一個空陣列存放結果
        let i = 0;
        function handleData(index, data) { //處理資料函式
            arr[index] = data;
            i++;
            if (i === promiseArrs.length) { //當i等於傳遞的陣列的長度時 
                resolve(arr); //執行resolve,並將結果放入
            }
        }
        for (let i = 0; i < promiseArrs.length; i++) { //迴圈遍歷陣列
            promiseArrs[i].then((data) => {
                handleData(i, data); //將結果和索引傳入handleData函式
            }, reject)
        }
    })
}
複製程式碼

下面我們逐個新增reace、resolve和reject方法。

新增Promise.race方法

race比賽競賽的意思,也就是誰跑的快就返回誰,不管你是成功還是失敗。跟all方法比較相似,但更簡單一些。循壞之後直接

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

經過上面的程式碼,以下兩個方法就顯得很簡單了,直接返回一個新的Promise,然後執行成功/失敗既可。

新增Promise.resolve方法

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

新增Promise.reject方法

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

如果想要用promises-aplus-tests測試是否符合Promise/A+規範的話,就要加上一段程式碼:

Promise.deferred = Promise.defer = function () { //這是promise的語法糖
  let dfd = {};
  dfd.promise = new Promise((resolve,reject)=>{
    dfd.resolve = resolve;
    dfd.reject = reject;
  })
  return dfd;
}
複製程式碼

在終端安裝完promises-aplus-tests後,在檔案的終端下執行promises-aplus-tests "檔名"既可

npm install promises-aplus-tests -g
promises-aplus-tests filename
複製程式碼

最後,解決下非同步的問題,完整程式碼如下:

function handlePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('circular reference'));
    }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        let called;
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    handlePromise(promise2, y, resolve, reject);
                }, r => {
                    if (called) return;
                    called = true;
                    reject(r)
                })
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }

    } else {
        resolve(x);
    }
}
class Promise {
    constructor(executor) {
        this.status = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.successStore = [];
        this.failStore = [];
        let resolve = (value) => {
            if (this.status === 'pending') {
                this.value = value;
                this.status = 'resolved';
                this.successStore.forEach(fn => fn());
            }
        }
        let reject = (reason) => {
            if (this.status === 'pending') {
                this.reason = reason;
                this.status = 'rejected';
                this.failStore.forEach(fn => fn());
            }
        }
        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : y => y;
        onRejected = typeof onRejected === 'function' ? onRejected : errr => {
            throw err;
        }
        let promise2; 
        if (this.status === 'resolved') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => { //非同步處理
                    try {
                        let x = onFulfilled(this.value);
                        handlePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            })
        }
        if (this.status === 'rejected') {
            promise2 = new Promise((resolve, reject) => {
                setTimeout(() => { //非同步處理
                    try {
                        let x = onRejected(this.reason);
                        handlePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0);
            })
        }
        if (this.status === 'pending') {
            promise2 = new Promise((resolve, reject) => {
                this.successStore.push(() => {
                    setTimeout(() => { //非同步處理
                        try {
                            let x = onFulfilled(this.value);
                            handlePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                })
                this.failStore.push(() => {
                    setTimeout(() => { //非同步處理
                        try {
                            let x = onRejected(this.reason);
                            handlePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    }, 0);
                })
            })
        }
        return promise2;
    }
}

Promise.all = function (promiseArrs) {
    return new Promise((resolve, reject) => {
        let arr = [];
        let i = 0;

        function processData(index, data) {
            arr[index] = data;
            i++;
            if (i === promiseArrs.length) {
                resolve(arr);
            }
        }
        for (let i = 0; i < promiseArrs.length; i++) {
            promiseArrs[i].then((data) => {
                processData(i, data);
            }, reject)
        }
    })
}

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

module.exports = Promise;
複製程式碼

現在,一個符合Promise/A+規範的Promise類已經完成了。見解有限,如有描述不準確之處,請幫忙及時指出。如有錯誤,一定會及時更正。

參考

segmentfault.com/a/119000000…
promisesaplus.com/
zhuanlan.zhihu.com/p/25178630

相關文章