從零開始手寫Promise

  面試的時候常常會問到Promise的使用;有的面試官再深刻一點,會繼續問是否瞭解Promise的實現方式,或者有沒有閱讀過Promise的源碼;今天咱們就來看一下,Promise在內部是如何實現來鏈式調用的。javascript

什麼是Promise

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

  Promise出現以前都是經過回調函數來實現,回調函數自己沒有問題,可是嵌套層級過深,很容易掉進回調地獄前端

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

  若是每次讀取文件後還要進行邏輯的判斷或者異常的處理,那麼整個回調函數就會很是複雜且難以維護。Promise的出現正是爲了解決這個痛點,咱們能夠把上面的回調嵌套用Promise改寫一下:java

const readFile = function(fileName){
    return new Promise((resolve, reject)=>{
        fs.readFile(fileName, (err, data)=>{
            if(err){
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
}

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

Promise規範

  promise最先是在commonjs社區提出來的,當時提出了不少規範。比較接受的是promise/A規範。可是promise/A規範比較簡單,後來人們在這個基礎上,提出了promise/A+規範,也就是實際上的業內推行的規範;es6也是採用的這種規範,可是es6在此規範上還加入了Promise.all、Promise.race、Promise.catch、Promise.resolve、Promise.reject等方法。es6

  咱們能夠經過腳原本測試咱們寫的Promise是否符合promise/A+的規範。將咱們實現的Promise加入如下代碼:面試

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

  而後經過module.exports導出,安裝測試的腳本:npm

npm install -g promises-aplus-tests
複製代碼

  在實現Promise的目錄執行如下命令:數組

promises-aplus-tests promise.js
複製代碼

  接下來,腳本會對照着promise/A+的規範,對咱們的腳原本一條一條地進行測試。更多規範可查看規範promise

Promise基本結構

  咱們先回顧一下,咱們平時都是怎麼使用Promise的:異步

var p = new Promise(function(resolve, reject){
    console.log('執行')
    setTimeout(function(){
        resolve(2)
    }, 1000)
})
p.then(function(res){
    console.log('suc',res)
},function(err){
    console.log('err',err)
})
複製代碼

  首先看出來,Promise是經過構造函數實例化一個對象,而後經過實例對象上的then方法,來處理異步返回的結果。同時,promise/A+規範規定了:

promise 是一個擁有 then 方法的對象或函數,其行爲符合本規範;

一個 Promise 的當前狀態必須爲如下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function Promise(executor) {
    var _this = this
    this.state = PENDING; //狀態
    this.value = undefined; //成功結果
    this.reason = undefined; //失敗緣由
    function resolve(value) {}
    function reject(reason) {}
}

Promise.prototype.then = function (onFulfilled, onRejected) {
};

module.exports = Promise;
複製代碼

  當咱們實例化Promise時,構造函數會立刻調用傳入的執行函數executor,咱們能夠試一下:

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

  所以在Promise中構造函數立馬執行,同時將resolve函數和reject函數做爲參數傳入:

function Promise(executor) {
    var _this = this
    this.state = PENDING; //狀態
    this.value = undefined; //成功結果
    this.reason = undefined; //失敗緣由
    function resolve(value) {}
    function reject(reason) {}
    executor(resolve, reject)
}
複製代碼

  可是executor也會可能存在異常,所以經過try/catch來捕獲一下異常狀況:

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

不可變

  promise/A+規範中規定,當Promise對象已經由等待態(Pending)改變爲執行態(Fulfilled)或者拒絕態(Rejected)後,就不能再次更改狀態,且終值也不可改變。

promise-states.png

  所以咱們在回調函數resolve和reject中判斷,只能是pending狀態的時候才能更改狀態:

function resolve(value) {
    if(_this.state === PENDING){
        _this.state = FULFILLED
        _this.value = value
    }
}
function reject(reason) {
    if(_this.state === PENDING){
        _this.state = REJECTED
        _this.reason = reason
    }
}
複製代碼

  咱們更改狀態的同時,將回調函數中成功的結果或者失敗的緣由都保存在對應的屬性中,方便之後來獲取。

then實現

  當Promise的狀態改變以後,無論成功仍是失敗,都會觸發then回調函數。所以,then的實現也很簡單,就是根據狀態的不一樣,來調用不一樣處理終值的函數。

Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
};
複製代碼

  在規範中也說了,onFulfilled和onRejected是可選的,所以咱們對兩個值進行一下類型的判斷:

onFulfilled 和 onRejected 都是可選參數。若是 onFulfilled 不是函數,其必須被忽略。若是 onRejected 不是函數,其必須被忽略

  代碼寫到這裏,貌似該有的實現方式都有了,咱們來寫個demo測試一下:

var myP = new Promise(function(resolve, reject){
    console.log('執行')
    setTimeout(function(){
        reject(3)
    }, 1000)
});

myP.then(function(res){
    console.log(res)
},function(err){
    console.log(err)
});
複製代碼

  然鵝,很遺憾,運行起來咱們發現只打印了構造函數中的執行,下面的then函數根本都沒有執行。咱們整理一下代碼的運行流暢:

flow.png

  當then裏面函數運行時,resolve因爲是異步執行的,尚未來得及修改state,此時仍是PENDING狀態;所以咱們須要對異步的狀況作一下處理。

支持異步

  那麼如何讓咱們的Promise來支持異步呢?咱們能夠參考發佈訂閱模式,在執行then方法的時候,若是當前仍是PENDING狀態,就把回調函數寄存到一個數組中,當狀態發生改變時,去數組中取出回調函數;所以咱們先在Promise中定義一下變量:

function Promise(executor) {
    this.onFulfilled = [];//成功的回調
    this.onRejected = []; //失敗的回調
}
複製代碼

  這樣,當then執行時,若是仍是PENDING狀態,咱們不是立刻去執行回調函數,而是將其存儲起來:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
    if(this.state === PENDING){
        typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
        typeof onRejected === 'function' && this.onRejected.push(onRejected)
    }
};
複製代碼

  存儲起來後,當resolve或者reject異步執行的時候就能夠來調用了:

function resolve(value) {
    if(_this.state === PENDING){
        _this.state = FULFILLED
        _this.value = value
        _this.onFulfilled.forEach(fn => fn(value))
    }
}
function reject(reason) {
    if(_this.state === PENDING){
        _this.state = REJECTED
        _this.reason = reason
        _this.onRejected.forEach(fn => fn(reason))
    }
}
複製代碼

  有童鞋可能會提出疑問了,爲何這邊onFulfilled和onRejected要存在數組中,直接用一個變量接收不是也能夠麼?下面看一個例子:

var p = new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve(4)
    }, 0)
})
p.then((res)=>{
    //4 res
    console.log(res, 'res')
})
p.then((res1)=>{
    //4 res1
    console.log(res1, 'res1')
})
複製代碼

  咱們分別調用了兩次then,若是是一個變量的話,最後確定只會運行後一個then,把以前的覆蓋了,若是是數組的話,兩個then都能正常運行。

  至此,咱們運行demo,就能如願以償的看到運行結果了;一個四十行左右的簡單Promise墊片就此完成了。這裏貼一下完整的代碼:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(executor) {
    var _this = this
    this.state = PENDING; //狀態
    this.value = undefined; //成功結果
    this.reason = undefined; //失敗緣由

    this.onFulfilled = [];//成功的回調
    this.onRejected = []; //失敗的回調
    function resolve(value) {
        if(_this.state === PENDING){
            _this.state = FULFILLED
            _this.value = value
            _this.onFulfilled.forEach(fn => fn(value))
        }
    }
    function reject(reason) {
        if(_this.state === PENDING){
            _this.state = REJECTED
            _this.reason = reason
            _this.onRejected.forEach(fn => fn(reason))
        }
    }
    try {
        executor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
    if(this.state === PENDING){
        typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
        typeof onRejected === 'function' && this.onRejected.push(onRejected)
    }
};
複製代碼

鏈式調用then

  相信上面的Promise墊片應該很容易理解,下面鏈式調用纔是Promise的難點和核心點;咱們對照promise/A+規範,一步一步地來實現,咱們先來看一下規範是如何來定義的:

then 方法必須返回一個 promise 對象

promise2 = promise1.then(onFulfilled, onRejected);

  也就是說,每一個then方法都要返回一個新的Promise對象,這樣咱們的then方法才能不斷的鏈式調用;所以上面的簡單墊片中then方法就不適用了,由於它什麼都沒有返回,咱們對其進行簡單的改寫,不論then進行什麼操做,都返回一個新的Promise對象:

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

  咱們繼續看then的執行過程:

  1. 若是 onFulfilled 或者 onRejected 返回一個值 x ,則運行下面的 Promise 解決過程:[[Resolve]](promise2, x)
  2. 若是 onFulfilled 或者 onRejected 拋出一個異常 e ,則 promise2 必須拒絕執行,並返回拒因 e
  3. 若是 onFulfilled 不是函數且 promise1 成功執行, promise2 必須成功執行並返回相同的值
  4. 若是 onRejected 不是函數且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的據因

  首先第一點,咱們知道onFulfilled和onRejected執行以後都會有一個返回值x,對返回值x處理就須要用到Promise解決過程,這個咱們下面再說;第二點須要對onFulfilled和onRejected進行異常處理,沒什麼好說的;第三和第四點,說的實際上是一個問題,若是onFulfilled和onRejected兩個參數沒有傳,則繼續往下傳(值的傳遞特性);舉個例子:

var p = new Promise(function(resolve, reject){
    setTimeout(function(){
        resolve(3)
    }, 1000)
});
p.then(1,1)
.then('','')
.then()
.then(function(res){
    //3
    console.log(res)
})
複製代碼

  這裏無論onFulfilled和onRejected傳什麼值,只要不是函數,就繼續向下傳入,直到有函數進行接收;所以咱們對then方法進行以下完善:

//_this是promise1的實例對象
var _this = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

var promise2 = new Promise((resolve, reject)=>{
    if(_this.state === FULFILLED){
        let x = onFulfilled(_this.value)
        resolvePromise(promise2, x, resolve, reject)
    } else if(_this.state === REJECTED){
        let x = onRejected(_this.reason)
        resolvePromise(promise2, x ,resolve, reject)
    } else if(_this.state === PENDING){
        _this.onFulfilled.push(()=>{
            let x = onFulfilled(_this.value)
            resolvePromise(promise2, x, resolve, reject)
        })
        _this.onRejected.push(()=>{
            let x = onRejected(_this.reason)
            resolvePromise(promise2, x ,resolve, reject)
        })
    }
})
複製代碼

  咱們發現函數中有一個resolvePromise,就是上面說的Promise解決過程,它是對新的promise2和上一個執行結果 x 的處理,因爲具備複用性,咱們把它抽成一個單獨的函數,這也是上面規範中定義的第一點。

  因爲then的回調是異步執行的,所以咱們須要把onFulfilled和onRejected執行放到異步中去執行,同時作一下錯誤的處理:

//其餘代碼略
if(_this.state === FULFILLED){
    setTimeout(()=>{
        try {
            let x = onFulfilled(_this.value)
            resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
            reject(error)
        }
    })
} else if(_this.state === REJECTED){
    setTimeout(()=>{
        try {                    
            let x = onRejected(_this.reason)
            resolvePromise(promise2, x ,resolve, reject)
        } catch (error) {
            reject(error)
        }
    })
} else if(_this.state === PENDING){
    _this.onFulfilled.push(()=>{
        setTimeout(()=>{
            try {                        
                let x = onFulfilled(_this.value)
                resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
                reject(error)
            }
        })
    })
    _this.onRejected.push(()=>{
        setTimeout(()=>{
            try {                        
                let x = onRejected(_this.reason)
                resolvePromise(promise2, x ,resolve, reject)
            } catch (error) {
                reject(error)
            }
        })
    })
}
複製代碼

Promise解決過程

Promise 解決過程是一個抽象的操做,其需輸入一個 promise 和一個值,咱們表示爲 [[Resolve]](promise, x),若是 x 有 then 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態;不然其用 x 的值來執行 promise 。

  這段話比較抽象,通俗一點的來講就是promise的解決過程須要傳入一個新的promise和一個值x,若是傳入的x是一個thenable的對象(具備then方法),就接受x的狀態:

//promise2:新的Promise對象
//x:上一個then的返回值
//resolve:promise2的resolve
//reject:promise2的reject
function resolvePromise(promise2, x, resolve, reject) {
}
複製代碼

  定義好函數後,來看具體的操做說明:

  1. x 與 promise 相等
    • 若是 promise 和 x 指向同一對象,以 TypeError 爲據因拒絕執行 promise
  2. x 爲 Promise
    1. 若是 x 處於等待態, promise 需保持爲等待態直至 x 被執行或拒絕
    2. 若是 x 處於執行態,用相同的值執行 promise
    3. 若是 x 處於拒絕態,用相同的據因拒絕 promise
  3. x 爲對象或函數
    1. 把 x.then 賦值給 then
    2. 若是取 x.then 的值時拋出錯誤 e ,則以 e 爲據因拒絕 promise
    3. 若是 then 是函數,將 x 做爲函數的做用域 this 調用之。傳遞兩個回調函數做爲參數,第一個參數叫作 resolvePromise ,第二個參數叫作 rejectPromise:
      1. 若是 resolvePromise 以值 y 爲參數被調用,則運行 [[Resolve]](promise, y)
      2. 若是 rejectPromise 以據因 r 爲參數被調用,則以據因 r 拒絕 promise
      3. 若是 resolvePromise 和 rejectPromise 均被調用,或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用
      4. 若是 then 不是函數,以 x 爲參數執行 promise
  4. 若是 x 不爲對象或者函數,以 x 爲參數執行 promise

  首先第一點,若是x和promise相等,這是一種什麼狀況呢,就是至關於把本身返回出去了:

var p = new Promise(function(resolve, reject){
    resolve(3)
});
//Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
var p2 = p.then(function(){
    return p2
})
複製代碼

  這樣會陷入一個死循環中,所以咱們首先要把這種狀況給排除掉:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Chaining cycle'));
    }
}
複製代碼

  接下來就是對不一樣狀況的判斷了,首先咱們把 x 爲對象或者函數的狀況給判斷出來:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Chaining cycle'));
    }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //函數或對象
    } else {
        //普通值
        resolve(x)
    }
}
複製代碼

  若是 x 爲對象或函數,就把 x.then 賦值給 then好理解,可是第二點取then有可能會報錯是爲何呢?這是由於須要考慮到全部出錯的狀況(防小人不防君子),若是有人實現Promise對象的時候使用Object.defineProperty()惡意拋錯,致使程序崩潰,就像這樣:

var Promise = {};
Object.defineProperty(Promise, 'then', {
    get: function(){
        throw Error('error')
    }
})
//Uncaught Error: error
Promise.then
複製代碼

  所以,咱們取then的時候也須要try/catch:

//其餘代碼略
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //函數或對象
    try {
        let then = x.then
    } catch(e){
        reject(e)
    }
}
複製代碼

  取出then後,回到3.3,判斷若是是一個函數,就將 x 做爲函數的做用域 this 調用,同時傳入兩個回調函數做爲參數。

//其餘代碼略
try {
    let then = x.then
    if(typeof then === 'function'){
        then.call(x, (y)=>{
            resolve(y)
        }, (r) =>{
            reject(r)
        })
    } else {
        resolve(x)
    }
} catch(e){
    reject(e)
}
複製代碼

  這樣,咱們的鏈式調用就能順利的調用起來了;可是還有一種特殊的狀況,若是resolve的y值仍是一個Promise對象,這時就應該繼續執行,好比下面的例子:

var p1 = new Promise((resolve, reject)=>{
    resolve('p1')
})
p1.then((res)=>{
    return new Promise((resolve, reject)=>{
        resolve(new Promise((resolve, reject)=>{
            resolve('p2')
        }))
    })
})
.then((res1)=>{
    //Promise {state: "fulfilled", value: "p2"}
    console.log(res1)
})
複製代碼

  這時候第二個then打印出來的是一個promise對象;咱們應該繼續遞歸調用resolvePromise(參考規範3.3.1),所以,最終resolvePromise的完整代碼以下:

function resolvePromise(promise2, x, resolve, reject){
    if(promise2 === x){
        reject(new TypeError('Chaining cycle'))
    }
    if(x && typeof x === 'object' || typeof x === 'function'){
        let used;
        try {
            let then = x.then
            if(typeof then === 'function'){
                then.call(x, (y)=>{
                    if (used) return;
                    used = true
                    resolvePromise(promise2, y, resolve, reject)
                }, (r) =>{
                    if (used) return;
                    used = true
                    reject(r)
                })
            } else {
                if (used) return;
                used = true
                resolve(x)
            }
        } catch(e){
            if (used) return;
            used = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}
複製代碼

  到這裏,咱們的Promise也可以完整的實現鏈式調用了;而後把代碼用promises-aplus-tests測試一下,完美的經過了872項測試。

test-promise.png

參考

promise/A規範(英文) promise/A+規範(英文) promise/A+規範(中文)

更多前端資料請關注公衆號【前端壹讀】

若是以爲寫得還不錯,請關注個人掘金主頁。更多文章請訪問謝小飛的博客

相關文章
相關標籤/搜索