一個超詳細的簡版Promise實現

這是一個簡版非規範的Promise實現,其中前置知識點比較重要,理解了前置知識點的內容,也就瞭解了Promise最基本的原理。數組

前置知識

這小節的核心是:把函數名當變量用。promise

如下三點理解以後,就可以實現簡單的Promise了。瀏覽器


  1. 類型爲Function的變量,能夠像函數同樣調用。

舉個栗子bash

let myFn = function () {};
複製代碼

你能夠直接調用異步

myFn(); 
複製代碼

和下面的代碼效果是同樣的函數

function myFn() {
    // code here...
}

myFn();
複製代碼
  1. 函數數組,即函數裏的每個元素都是函數,能夠用遍歷調用。
let fnArray = [];
let fn1 = () => { console.log(1) };
let fn2 = () => { console.log(2) };
fnArray.push(fn1);
fnArray.push(fn2);
fnArray.forEach(cb => cb());
複製代碼

這段代碼的運行結果是ui

1
2
複製代碼
  1. 函數能夠看成參數傳遞
function showYouFn(fn1, fn2) {
    // 調用
    fn1();
    fn2();
}

let myFn1 = () => { console.log(3); };
let myFn2 = () => { console.log(4); };

showYouFn(myFn1, myFn2);
複製代碼

回想一下:把函數名當變量用。this

實現

下面是一個簡陋得不像Promise的Promise實現spa

function MyPromise(fn) {
    function resolve(value) {
        console.log(value);
    }
    function reject(value) {
        console.log(value);
    }
    
    fn(resolve, reject);
}
複製代碼

如今你就能夠用上自定義的Promise了prototype

new MyPromise((resolve, reject) => {
    setTimeout(()=>{
        resolve('你看到我啦!');
    }, 2000);
});
複製代碼

將會在2秒後輸出

你看到我啦!
複製代碼

下面的代碼效果和上面的同樣,寫出來是爲了再次強調:函數當變量用。

let promiseFn = (resolve, reject) => {
    setTimeout(()=>{
        resolve('你看到我啦!');
    }, 2000);
};
new MyPromise(promiseFn);
複製代碼

解釋一下總體代碼:

MyPromise 中的參數 fn 是須要用戶傳入的自定義函數(該函數須要接收兩個參數)。

MyPromise 中的內部還有兩個函數 resolvereject,其中 resolve 表示用戶的異步任務成功時應該調用的函數,reject 表示用戶的異步任務失敗時該調用的函數。

那用戶如何調用 resolvereject 呢?

很簡單,把兩個函數看成 fn的參數傳遞出去便可。

因此 MyPromise 內部在調用 fn 時會把 resolvereject看成參數傳遞給 fn

而後用戶在自定義函數內調用 resolvereject 來通知 MyPromise 異步任務已經執行完了。


建議複製下面的代碼去控制檯試試

function MyPromise(fn) {
    function resolve(value) {
        console.log(value);
    }
    function reject(value) {
        console.log(value);
    }
    
    fn(resolve, reject);
}

new MyPromise((resolve, reject) => {
    setTimeout(()=>{
        resolve('你看到我啦!');
    }, 2000);
});
複製代碼

經過上面的代碼能夠發現一個問題,咱們不知道Promise的異步任務進行到哪一步了、是成功仍是失敗了。

因此增長三個狀態用來標識一個Promise的異步任務進行到何種程度了。

pendingresolvedrejected 分別表示 執行中、已完成、已失敗。

而後經過觀察用戶調用的是 resolve 仍是 reject 能夠判斷當前Promise的狀態。

那麼會有三種狀況:

  1. 在用戶調用 resolvereject 以前狀態是 pending
  2. 用戶調用 resolve 時,狀態將變爲 resolved
  3. 用戶調用 reject 時,狀態將變爲 rejected

下面進行代碼的改造,定義了三個常量表示狀態以及一個變量 state 用來存儲當前狀態。

而且當 resolve 被調用時將 state 修改成 resolved

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

function MyPromise(fn) {

    const that = this;
    // 初始時狀態爲執行中,即pending
    that.state = PENDING;

    function resolve(value) {
        console.log(value);
        // resolve被調用時修改狀態
        that.state = RESOLVED;
    }
    function reject(value) {
        console.log(value);
        // reject被調用時修改狀態
        that.state = REJECTED;
    }
    
    fn(resolve, reject);
}
複製代碼

OK,如今已經能知道Promise當前所處的狀態了,可是任務完了得拿到結果吧,MyPromiseresolve 被調用,那也只是MyPromise知道任務完成了,用戶還不知道呢。

因此咱們須要回調函數告訴用戶,是的,其實就是回調函數。

這時候就輪到 then 方法出場了,用戶經過then方法傳入回調函數, MyPromise 將在成功調用 resolve 時調用用戶傳入的回調函數。

開始改造代碼,MyPromise 內部須要變量存儲回調函數,then 方法的做用就是將用戶傳入的回調函數賦予 MyPromise 內的變量。

因此 then 方法長這樣,接收兩個參數,一個是成功時的回調函數,一個是失敗時的回調函數。

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';

function MyPromise(fn) {
    const that = this;
    that.state = PENDING;
    
    // 兩個存儲回調函數的變量
    that.resolvedCallback;
    that.rejectedCallback;

    function resolve(value) {
        console.log(value);
        that.state = RESOLVED;
        // 調用用戶的回調函數,並告知結果
        that.resolvedCallback && that.resolvedCallback(value);
    }
    function reject(value) {
        console.log(value);
        that.state = REJECTED;
        // 調用用戶的回調函數,並告知結果
        that.rejectedCallback && that.rejectedCallback(value);
    }
    
    fn(resolve, reject);
}


MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const that = this;
    that.resolvedCallback = onFulfilled;
    that.rejectedCallback = onRejected;
}
複製代碼

是的,一個簡版Promise幾乎大功告成,讓咱們再試試在瀏覽器執行以下代碼(注意我刪除了 resolvereject 裏的console語句)

(function () {
    const PENDING = 'pending';
    const RESOLVED = 'resolved';
    const REJECTED = 'rejected';
    function MyPromise(fn) {
        const that = this;
        that.state = PENDING;
        
        // 兩個存儲回調函數的變量
        that.resolvedCallback;
        that.rejectedCallback;
    
        function resolve(value) {
            that.state = RESOLVED;
            // 調用用戶的回調函數,並告知結果
            that.resolvedCallback && that.resolvedCallback(value);
        }
        function reject(value) {
            that.state = REJECTED;
            // 調用用戶的回調函數,並告知結果
            that.rejectedCallback && that.rejectedCallback(value);
        }
        
        fn(resolve, reject);
    }
    
    
    MyPromise.prototype.then = function(onFulfilled, onRejected) {
        const that = this;
        that.resolvedCallback = onFulfilled;
        that.rejectedCallback = onRejected;
    }
    
    new MyPromise((resolve, reject) => {
        setTimeout(() => {
            resolve('你又看到我啦~');
        }, 2000);
    }).then(value => {
        console.log(value);
    });
})();
複製代碼

上面的代碼,用法上已經和Promise長得差很少了,可是若是咱們屢次調用 then 方法呢?

是的,只有最後一個 then 方法裏的回調函數能執行,這固然無法知足咱們的須要。

因而,將兩個回調函數改爲函數數組(請回想一下前置知識),並在狀態更改時遍歷調用回調函數。

改造後的代碼以下:

const PENDING = 'pending';
const RESOLVED = 'resolved';
const REJECTED = 'rejected';
function MyPromise(fn) {
    const that = this;
    that.state = PENDING;
    
    // 注意這裏變成了兩個數組
    that.resolvedCallbackArray = [];
    that.rejectedCallbackArray = [];

    function resolve(value) {
        that.state = RESOLVED;
        // 遍歷用戶的回調函數,告知結果
        that.resolvedCallbackArray.forEach(cbFn => cbFn(value));
    }
    function reject(value) {
        that.state = REJECTED;
        // 遍歷用戶的回調函數,告知結果
        that.rejectedCallbackArray.forEach(cbFn => cbFn(value));
    }
    
    fn(resolve, reject);
}
    
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const that = this;
    that.resolvedCallbackArray.push(onFulfilled);
    that.rejectedCallbackArray.push(onRejected);
    
    // 這裏是爲了鏈式調用,即myPromise.then().then().then()...
    return that;
}
複製代碼

如今咱們能夠屢次調用 then 方法了。試試在控制檯運行下面的代碼:

(function () {
    const PENDING = 'pending';
    const RESOLVED = 'resolved';
    const REJECTED = 'rejected';
    function MyPromise(fn) {
        const that = this;
        that.state = PENDING;
        
        // 注意這裏變成了兩個數組
        that.resolvedCallbackArray = [];
        that.rejectedCallbackArray = [];
    
        function resolve(value) {
            that.state = RESOLVED;
            // 遍歷用戶的回調函數,告知結果
            that.resolvedCallbackArray.forEach(cbFn => cbFn(value));
        }
        function reject(value) {
            that.state = REJECTED;
            // 遍歷用戶的回調函數,告知結果
            that.rejectedCallbackArray.forEach(cbFn => cbFn(value));
        }
        
        fn(resolve, reject);
    }
        
    MyPromise.prototype.then = function(onFulfilled, onRejected) {
        const that = this;
        that.resolvedCallbackArray.push(onFulfilled);
        that.rejectedCallbackArray.push(onRejected);
        return that;
    }
    
    new MyPromise((resolve, reject) => {
        setTimeout(() => {
            resolve('你又看到我啦~');
        }, 2000)
    }).then(value => {
        console.log('第一次', value);
    }).then(value => {
        console.log('第二次', value);
    });
})();
複製代碼

上面已是簡版Promise的實現了。

可是咱們還能夠更完善一點,加強 MyPromise 的健壯性。

例如,若用戶自定義函數在執行過程當中發生了錯誤,會中斷程序的執行,因而咱們增長try...catch...語句,並在發生錯誤時主動執行reject函數告知用戶。

try {
    fn(resolve, reject);
} catch (e) {
    reject(e);
}
複製代碼

又或者,對參數進行校驗,狀態進行判斷等,以 then爲例,若用戶傳入的參數不是函數呢? 或者Promise的狀態已經時rejectedresolved,此時調用then呢?

改造 then 後代碼以下:

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if(typeof onRejected !== 'function') {
        onRejected = v => v;
    }
    if(typeof onFulfilled !== 'function') {
        onFulfilled = v => { throw r };
    }
    const that = this;
    if (that.state === PENDING) {
        that.resolvedCallbacks.push(onFulfilled)
        that.rejectedCallbacks.push(onRejected)
    }
    if (that.state === RESOLVED) {
        onFulfilled(that.value)
    }
    if (that.state === REJECTED) {
        onRejected(that.value)
    }
}
複製代碼

還有其餘方法在理解了基本原理以後能夠本身根據規範改造,本文到此告一段落,感謝閱讀。

相關文章
相關標籤/搜索