Promise原理詳解 入門Promise的正確姿式

參考文章:深刻理解 Promise【翻譯】Promises/A+規範html

入門Promise的正確姿式中咱們已經瞭解到Promise的基本用法。那麼如今給你一個需求:根據Promise的用法和Promise/A+規範,實現一個本身的Promise函數。編程

一、Promise構造函數的結構

根據Promise的用法咱們知道:在new一個Promise函數的時候,Promise函數必須接受一個函數做爲參數。咱們暫且把這個參數函數稱爲執行器。數組

執行器提供兩個參數(resolve,reject),且內部有狀態機制(pending,resolved,rejected)。Promise構造函數的原型上有then方法。promise

咱們再來看一下PromiseA+標準中是怎樣規定的:緩存

  1. Promise對象初始狀態爲 Pending,在被 resolve 或 reject 時,狀態變爲 Fulfilled或 Rejected
  2. resolve接收成功的數據,reject接收失敗或錯誤的數據
  3. Promise對象必須有一個 then 方法,且只接受兩個函數參數 onFulfilledonRejected

由以上標準就容易就能實現這個類的大體結構。app

開始擼:異步

var PENDING = 'pending',
    RESOLVED = 'resolved',
    REJECTED = 'rejected',
function Promise(executor) {
    //養成良好的編程習慣,先對參數作一個容錯處理
    if (executor && typeof executor != 'function') {
        throw new Error('Promise is not a function');
    }
    let _this = this;//緩存this
    _this.status = PENDING;//當前Promise的狀態
    _this.value = undefined;//Promise成功執行時要傳遞給回調的數據,默認爲undefined
    _this.reason = undefined;//Promise失敗執行時傳遞給毀掉的緣由,默認爲undefined
    function resolve(value) { //內置一個resolve方法
        if (_this.status == PENDING) {
            _this.status = RESOLVED;//當調用resolve時,將Promise的狀態改成resoled
            _this.value = value;//保存成功調用時傳遞進來的數據
        }
    }
    function reject(reason) {
        if (_this.status == PENDING) {
            _this.status = REJECTED;//當調用reject時,將Promise的狀態改成rejected
            _this.reason = reason;//保存失敗調用時傳遞進來的緣由
        }
    }
    executor(resolve,reject);
    
}
//then方法能夠接受兩個函數參數,分別表示當前Promise執行成功時的調用onFulfilled和執行失敗時調用onRejected
Promise.prototype.then = function(onFulfilled,onRejected) {
    let _this = this,//緩存this,保不齊後面會用到,固然若是你不想緩存this,也能夠在後面使用箭頭函數
    if (_this.status == RESOLVED) {
        onFulfilled(_this.value);
    }
    if (_this.status == REJECTED) {
        onFulfilled(_this.reason);
    }
}

看起來不錯,但回調函數是當即執行的,也就是說上面實現的Promise只支持同步代碼,而沒法進行異步操做,好比這樣是不行的函數

let p = new Promise(function(resolve, reject){
  setTimeout(function(){
    resolve('成功執行了!'); 
  }, 1000)
})
p.then(function(data){
  console.log('成功', data)
},function(err){
  console.log('失敗', err)
})
// 不會輸出任何代碼

緣由是:咱們在then函數中只對成功態和失敗態進行了判斷,而實例被new時,執行器中的代碼會當即執行,但setTimeout中的代碼將稍後執行,也就是說,then方法執行時,Promise的狀態沒有被改變依然是pending態。post

因此咱們要對pending態也作判斷,而因爲代碼多是異步的,因此回調函數就不應被當即執行,因此咱們就要想辦法把回調函數進行緩存,當狀態改變後(由pending態變成resolved或rejected態),再執行相應的函數。this

那麼狀態在何時改變呢:很顯然,在執行resolve或rejected函數的時候,狀態會發生改變。

而且,then方法是能夠屢次使用的,因此要能存多個回調,那麼這裏咱們用一個數組來存儲多個回調函數。

1.一、實現異步

在實例上掛載兩個參數

_this.onResolvedCallbacks = []; // 存放then成功的回調
_this.onRejectedCallbacks = []; // 存放then失敗的回調

咱們再給then方法加一個pending時的判斷

if(_this.status === 'pending'){
    // 每一次then時,若是是等待態,就把回調函數push進數組中,何時改變狀態何時再執行
    _this.onResolvedCallbacks.push(function(){ // 這裏用一個函數包起來,是爲了後面加入新的邏輯進去
        onFulfilled(_this.value)
    })
    _this.onRejectedCallbacks.push(function(){ // 同理
        onRjected(_this.reason)
    })
}

下一步要分別在resolve和reject方法里加入執行數組中存放的函數的方法,修改一下上面的resolve和reject方法

function resolve(value) {
    if (_this.status === 'pending') { 
        _this.status = 'resolved'
        _this.value = value
        _this.onResolvedCallbacks.forEach(function(fn){ // 當成功的函數被調用時,以前緩存的回調函數會被一一調用
            fn()
        })
    }
}
function reject(reason) {
    if (_this.status === 'pending') {
        _this.status = 'rejected'
        _this.reason = reason
        _this.onRejectedCallbacks.forEach(function(fn){// 當失敗的函數被調用時,以前緩存的回調函數會被一一調用
            fn()
        })
    }
}

如今能夠執行異步任務了,也能夠給一個Promise函數屢次then了。

1.二、錯誤處理

上面的代碼看似很完美,前提是整個Promise函數可以正確執行,那樣就沒reject函數什麼事了。但咱們必需要考慮的一種狀況是:當Promise函數執行出錯時怎麼辦,因此一個健壯的Promise函數是應該有錯誤處理機制的。因此咱們應該在Promise的代碼體中加入try catch,若是出現異常,則捕捉錯誤交給reject。

咱們實現一下,思路很簡單,在執行器執行時進行try catch

try{
    executor(resolve, reject)        
}catch(e){ // 若是捕獲發生異常,直接調失敗,並把參數穿進去
    reject(e)
}

二、實現then的鏈式調用(難點)

上面說過了,then能夠鏈式調用,也是這一點讓Promise十分好用,固然這部分源碼也比較複雜

咱們知道jQuery實現鏈式調用是return了一個this,但Promise不行,爲何不行?

由於then函數內返回的是一個新的Promise對象。咱們看一下標準是如何定義的:

標準中規定:

  1. then 方法必須返回一個新的 Promise實例(ES6中的標準,Promise/A+中沒有明確說明)

  2. 爲了保證 then中回調的執行順序,onFulfilled 或 onRejected 必須異步調用

因此咱們要作的就是:在then方法中先定義一個新的Promise,取名爲promise2,而後在三種狀態下分別用promise2包裝一下,在調用onFulfilled時用一個變量x接收返回值,try catch一下代碼,沒錯就調resolve傳入x,有錯就調reject傳入錯誤,最後再把promise2給return出去,就能夠進行鏈式調用了

//修改then
Promise.prototype.then = function(onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) {return vaule};
    onRejected = typeof onRejected === 'function' ? onFulfilled : function(err) {throw err};

    let _this = this;//緩存this,保不齊後面會用到,固然若是你不想緩存this,也能夠在後面使用箭頭函數
    let promise2;
    if (_this.status === RESOLVED) {
        promise2 = new Promise(function(resolve,reject) {
            try {
                //onFulfilled函數的執行狀況要考慮多種狀況,後面會細說
                let x = onFulfilled(_this.value);//將執行onFulfilled的返回值傳給x,這裏須要注意的是執行過程當中有可能會出錯
                resolve(x);
            } catch(e) {
                reject(e);
            }
        })
        
    }
    if (_this.status === REJECTED) {
        promise2 = new Promise(function(resolve,reject) {
            try {
                //onFulfilled函數的執行狀況要考慮多種狀況,後面會細說
                let x = onRejected(_this.reason);//將執行onFulfilled的返回值傳給x(即此次then函數執行的返回值),這裏須要注意的是執行過程當中有可能會出錯
                resolve(x);//這個x會被下一次的then函數接收到
            } catch(e) {
                reject(e);
            }
        })
        
    }

    if(_this.status === PENDING){
        promise2 = new Promise(function(resolve,reject) {
        // 每一次then時,若是是等待態,就把回調函數push進數組中,何時改變狀態何時再執行
        _this.onResolvedCallbacks.push(function(){ // 這裏用一個函數包起來,是爲了後面加入新的邏輯進去
            try {
                let x = onFulfilled(_this.value);
                resolve(x);
            } catch(e) {
                reject(e);
            }
        })
        _this.onRejectedCallbacks.push(function(){ // 同理
            try {
                let x = onRejected(_this.reason);
                resolve(x);
            } catch(e) {
                reject(e);
            }
        })
        })
        
    }
}

上面的實現雖然能用,可是很粗糙。

在接下的分析以前,我但願你們可以清晰的明白x的值表明什麼:x表示的是上一次的then函數或promise函數執行結果的返回值,這個x的值會被resolve(x),做爲下一次then函數調用時的參數。

明確了x的值之後,接下來對onFulfilled和onRejected函數可能出現的狀況作一個列舉,這也是爲了咱們的Promise函數能作到最大的容錯率:

  1. 前一次then返回一個普通值,字符串數組對象這些東西,都沒問題,只需傳給下一個then,剛纔的方法就夠用。
  2. 前一次then返回的是一個Promise,是正常的操做,也是Promise提供的語法糖,咱們要想辦法判斷到底返回的是啥。
  3. 前一次then返回的是一個Promise,其中有異步操做,也是理所固然的,那咱們就要等待他的狀態改變,再進行下面的處理。
  4. 前一次then返回的是本身自己這個Promise
var p1 = p.then(function(){// 這裏得用var,let因爲做用域的緣由會報錯undefined
  return p1  
})

  5.前一次then返回的是一個別人本身隨便寫的Promise,這個Promise多是個有then的普通對象,好比{then:'哈哈哈'},也有可能在then裏故意拋錯(這種蛋疼的 操做咱們也要考慮進去)。好比他這樣寫

let promise = {}
Object.defineProperty(promise,'then',{
    value: function(){
        throw new Error('報錯氣死你')
    }
})
// 若是返回這東西,咱們再去調then方法就確定會報錯了

  6.調resolve的時候再傳一個Promise下去,咱們還得處理這個Promise。

p.then(function(data) {
    return new Promise(function(resolve, reject) {
      resolve(new Promise(function(resolve,reject){
        resolve(1111)
      }))
    })
})

  7.可能既調resolve又調reject,得忽略後一個。

  8.光then,裏面啥也不寫。

問題8最好解決,咱們只要手動添加一個onFulfilled和onRejected進去就行了。

對於問題1-7,咱們能夠採起統一的以爲方案,定義一個函數來判斷和處理這一系列的狀況,官方給出了一個叫作resolvePromise的函數。

因此,咱們進一步來完善then方法。

Promise.prototype.then = function(onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(value) {return value};
    onRejected = typeof onRejected === 'function' ? onRejected : function(err) {throw err};
   //這裏正好解釋了在正式的Promise函數中,咱們爲何能夠不寫onRejected函數,
   //由於then方法內部會幫咱們封裝好一個onRejected函數,用來拋出上一次then或Promise執行出錯的信息,這也是爲何能夠在最後執行catch方法的緣由 let _this
= this;//緩存this,保不齊後面會用到,固然若是你不想緩存this,也能夠在後面使用箭頭函數 let promise2; if (_this.status === RESOLVED) { promise2 = new Promise(function(resolve,reject) { try { //onFulfilled函數的執行狀況要考慮多種狀況,後面會細說 let x = onFulfilled(_this.value);//將執行onFulfilled的返回值傳給x,這裏須要注意的是執行過程當中有可能會出錯 resolvePromise(promise2,x,resolve,reject); } catch(e) { reject(e); } }) } if (_this.status === REJECTED) { promise2 = new Promise(function(resolve,reject) { try { //onFulfilled函數的執行狀況要考慮多種狀況,後面會細說 let x = onRejected(_this.reason);//將執行onFulfilled的返回值傳給x(即此次then函數執行的返回值),這裏須要注意的是執行過程當中有可能會出錯 resolvePromise(promise2,x,resolve,reject);//這個x會被下一次的then函數接收到 } catch(e) { reject(e); } }) } if(_this.status === PENDING){ promise2 = new Promise(function(resolve,reject) { // 每一次then時,若是是等待態,就把回調函數push進數組中,何時改變狀態何時再執行 _this.onResolvedCallbacks.push(function(){ // 這裏用一個函數包起來,是爲了後面加入新的邏輯進去 try { let x = onFulfilled(_this.value); resolvePromise(promise2,x,resolve,reject); } catch(e) { reject(e); } }) _this.onRejectedCallbacks.push(function(){ // 同理 try { let x = onRejected(_this.reason); resolvePromise(promise2,x,resolve,reject); } catch(e) { reject(e); } }) }) } return promise2; }

定義resolvePromise方法:

function resolvePromise(promise2, x, resolve, reject) {
    // 接受四個參數: 新的Promise、返回值,成功和失敗的回調
    // 有可能這裏返回的x是別人的promise
    // 儘量容許其餘亂寫
    if (promise2 === x) { //這裏應該報一個類型錯誤,來解決問題4
        return reject(new TypeError('循環引用了'))
    }
    // 看x是否是一個promise,promise應該是一個對象
    let called = false; // 表示是否調用過成功或者失敗,用來解決問題7
    //下面判斷上一次then返回的是普通值仍是函數,來解決問題一、2
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        // 多是promise {},看這個對象中是否有then方法,若是有then我就認爲他是promise了
        try {
            let then = x.then;// 保存一下x的then方法
            if (typeof then === 'function') {
                // 成功
                //用call方法修改指針爲x,不然this指向window
                then.call(x, function (y) {//若是x是一個Promise對象,y參數表示x執行後的resolve值
                    //console.log(y);
                    if (called) return //若是調用過就return掉
                    called = true
                    // y可能仍是一個promise,在去解析直到返回的是一個普通值
                    resolvePromise(promise2, y, resolve, reject)//遞歸調用,解決了問題6
                }, function (err) { //失敗時執行的函數
                    if (called) return
                    called = true
                    console.log('r');
                    reject(err);
                })
            } else {//若是x不是一個Promise對象,則直接resolve(x)
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e);
        }
    } else { // 說明是一個普通值
        resolve(x); // 表示成功了
    }
}

2.一、catch方法

因爲catch方法是then(null, onRejected)的語法糖,因此這裏也很好實現。

Promise.prototype.catch = function(onRejected) {
    return this.then(null,onRejected);
}

 至此,一個簡易粗糙的Promise函數已經實現了。同窗們能夠本身用本身寫的這個Promise函數驗證一下是不是否可行。

相關文章
相關標籤/搜索