性感的Promise,擁抱ta然後扒光ta

rocYoung發表於2019-03-04

Promise,js非同步程式設計的流行解決方案,相比於古老的回撥函式等方式,它更科學,更優雅。它來自民間,後被官方招安。

本文將從介紹用法開始,一步步瞭解Promise,探究原始碼,最終根據官方規範手寫一個Promise。

讓我們先擁抱ta,再扒光ta!

我想在你身上,做春天在櫻桃樹身上做的事情。——巴勃羅·聶魯達

1. How Promise?

  • 建立Promise

首先來看看promise的用法,從名字可以看出它是個建構函式,所以我們得new它,得到一個Promise例項p,我們列印p看看

let p = new Promise
console.log(p) // TypeError: Promise resolver undefined is not a function
複製程式碼
  • 引數

報錯資訊告訴我們,Promise需要一些引數,這裡需要一個函式(我們叫它執行器)作為引數,該函式有兩個引數————resolve和reject,這兩個引數也是函式(由js引擎提供),我們可以在Promise內部呼叫,當非同步操作成功時,呼叫resolve,否則reject。

let p =new Promise(function(resolve, reject){
    if(/* 非同步操作成功 */){
        resolve(data)
    }else{
        reject(err)
    }
})
複製程式碼
  • state

現在我們需要知道一個重要概念,Promise是有“狀態”的,分別是pending(等待態)、fulfilled(成功態)、rejected(失敗態),pending可以轉換為fulfilled或rejected,但fulfilled和rejected不可相互轉化。

  • resolve/reject 方法

resolve方法可以將pending轉為fulfilled,reject方法可以將pending轉為rejected。

  • then方法

通過給Promise示例上的then方法傳遞兩個函式作為引數,可以提供改變狀態時的回撥,第一個函式是成功的回撥,第二個則是失敗的回撥。

p.then(function(data){ // resolve方法會將引數傳進成功的回撥
    console.log(data)  
}, function(err){      // reject方法會將失敗的資訊傳進失敗的回撥
    console.log(err)
})
複製程式碼
性感的Promise,擁抱ta然後扒光ta 舉個例子
let p = new Promise(function(resolve, reject){
    setTimeout(function(){
        let num = Math.random()
        if (num > 0.5) {
            resolve(num)
        }else{
            reject(num)
        }
    }, 1000)
})
p.then(function(num){
    console.log('大於0.5的數字:', num)
},function(num){
    console.log('小於等於0.5的數字', num)
})
// 執行第一次:小於等於0.5的數字 0.166162996031475
// 執行第二次:大於0.5的數字: 0.6591451548308984
...
複製程式碼

在Promise執行器中我們進行了一次非同步操作,並在我們覺得合適的時候呼叫成功或失敗的回撥函式,並拿到了想要的資料以進行下一步操作

  • 鏈式呼叫

除此之外,每一個then方法都會返回一個新的Promise例項(不是原來那個),讓then方法支援鏈式呼叫,並可以通過返回值將引數傳遞給下一個then

p.then(function(num){
    return num
},function(num){
    return num
}).then(function(num){
    console.log('大於0.5的數字:', num)
},function(num){
    console.log('小於等於0.5的數字', num)
})
複製程式碼
  • catch方法

catch方法等同於.then(null, reject),可以直接指定失敗的回撥(支援接收上一個then發生的錯誤)

  • Promise.all()

這可能是個很有用的方法,它可以統一處理多個Promise

Promise.all能將多個Promise例項包裝成一個Promise例項

let Promise1 = new Promise(function(resolve, reject){})
let Promise2 = new Promise(function(resolve, reject){})
let Promise3 = new Promise(function(resolve, reject){})

let p = Promise.all([Promise1, Promise2, Promise3])

p.then(funciton(){
  // 三個都成功則成功  
}, function(){
  // 只要有失敗,則失敗 
})
複製程式碼

這個組合後的Promise例項和普通例項一樣,有三種狀態,這裡有組成它的幾個小Promise的狀態決定 :

1、當Promise1, Promise2, Promise3的狀態都為成功態,則p為成功態; 2、當Promise1, Promise2, Promise3中有任意一個為失敗態,則p為失敗態;

  • Promise.race()

與all方法類似,也可以講多個Promise例項包裝成一個新的Promise例項

不同的是,all時大Promise的狀態由多個小Promise共同決定,而race時由第一個轉變狀態的小Promise的狀態決定,第一個是成功態,則轉成功態,第一個失敗態,則轉失敗態

  • Promise.resolve()

可以生成一個成功的Promise

Promise.resolve('成功')等同於new Promise(function(resolve){resolve('成功')})

  • Promise.reject()

可以生成一個失敗的Promise

Promise.reject('出錯了')等同於new Promise((resolve, reject) => reject('出錯了'))

上述用法不夠詳細,下面的程式碼會更容易理解

2. Why Promise?

以jquery的ajax為例(@1.5.0版本以前,後來jquery也引入了Promise的概念),看看從前我們是如何解決非同步問題的。

$.get('url', {data: data}, function(result){
    console.log('成功', result)// 成功的回撥,result為非同步拿到的資料
});
複製程式碼

看起來還可以?

想象一個場景,當我們需要傳送多個非同步請求,而請求之間相互關聯相互依賴,沒有請求1就不會有請求2,沒有請求2就不會有請求3........

這時我們需要這樣寫

$.get('url', {data: data}, function(result1){
    $.get('url', {data: result1}, function(result2){
        $.get('url', {data: result2}, function(result3){
            $.get('url', {data: result3}, function(result4){
                ......
                $.get('url', {data: resultn}, function(resultn+1){
                    console.log('成功')
                }
            }
        }
    }
});
複製程式碼

這樣的話,我們就掉入了傳說中的回撥地獄,萬劫不復,不能自拔。

這種程式碼,難以維護和除錯,一旦出現bug,牽一髮而動全身。

下面我們看看Promise是如何解決的,我們以node中的fs訪問檔案舉例

先建立三個相互依賴的txt檔案

1.txt的內容:

2.txt
複製程式碼

2.txt的內容:

3.txt
複製程式碼

3.txt的內容:

完成
複製程式碼

js程式碼:

let readFile = require('fs').readFile; // 載入node內建模組fs 利用readFile方法非同步訪問檔案
function getFile(url){  // 建立一個讀取檔案方法
    return new Promise(function(resolve, reject){  // 返回一個Promise物件
        readFile(url, 'utf8', function(err,data){  // 讀取檔案  
            resolve(data)  // 呼叫成功的方法
        })
    })
}
getFile('1.txt').then(function(data){  // then方法進行鏈式呼叫
    console.log(data)  // 2.txt
    return getFile(data)    //拿到了第一次的內容用來請求第二次
}).then(function(data){
    console.log(data)  // 3.txt
    return getFile(data)  //拿到了第二次的內容用來請求第三次
}).then(function(data){
    console.log(data)  // 完成
})
複製程式碼

(這裡我們先不必搞懂程式碼,下面會介紹具體用法)

看起來多了幾行程式碼[尷尬],但我們通過建立一個讀取函式返回一個Promise物件,再利用Promise自帶的.then方法,將巢狀的非同步程式碼弄得看起來像同步一樣,這樣的話,出現問題可以輕易的除錯和修改。

3. What Promise?

接下來是本文的重頭戲,根據PromiseA+(Promise的官方標準)動手實現一個180行左右程式碼的promise,功能可實現多數(then catch all race resolve reject),這裡會將的比較詳細,一步一步理清思路。

  • 實現resolve、reject方法,then方法和狀態機制

根據使用方法我們可以知道,Promise是一個需要接受一個執行器的建構函式,執行器提供兩個方法,內部有狀態機制,原型鏈上有then方法。

開始擼:

// myPromise
function Promise(executor){ //executor是一個執行器(函式)
    let _this = this // 先快取this以免後面指標混亂
    _this.status = 'pending' // 預設狀態為等待態
    _this.value = undefined // 成功時要傳遞給成功回撥的資料,預設undefined
    _this.reason = undefined // 失敗時要傳遞給失敗回撥的原因,預設undefined
    function resolve(value) { // 內建一個resolve方法,接收成功狀態資料
        // 上面說了,只有pending可以轉為其他狀態,所以這裡要判斷一下
        if (_this.status === 'pending') { 
            _this.status = 'resolved' // 當呼叫resolve時要將狀態改為成功態
            _this.value = value // 儲存成功時傳進來的資料
        }
    }
    function reject(reason) { // 內建一個reject方法,失敗狀態時接收原因
        if (_this.status === 'pending') { // 和resolve同理
            _this.status = 'rejected' // 轉為失敗態
            _this.reason = reason // 儲存失敗原因
        }
    }
    executor(resolve, reject) // 執行執行器函式,並將兩個方法傳入
}
// then方法接收兩個引數,分別是成功和失敗的回撥,這裡我們命名為onFulfilled和onRjected
Promise.prototype.then = function(onFulfilled, onRjected){
    let _this = this;   // 依然快取this
    if(_this.status === 'resolved'){  // 判斷當前Promise的狀態
        onFulfilled(_this.value)  // 如果是成功態,當然是要執行使用者傳遞的成功回撥,並把資料傳進去
    }
    if(_this.status === 'rejected'){ // 同理
        onRjected(_this.reason)
    }
}
module.exports = Promise  // 匯出模組,否則別的檔案沒法使用
複製程式碼

注意:上面程式碼的命名不是隨便起的,像onFulfilled和onRjected,是嚴格按照Promise/A+規範走的,不信你看圖

性感的Promise,擁抱ta然後扒光ta

這樣我們就實現了第一步,可以建立Promise例項並使用then方法了,測試一下

let Promise = require('./myPromise')  // 引入模組
let p = new Promise(function(resolve, reject){
  resolve('test')
})
p.then(function(data){
  console.log('成功', data)
},function(err){
  console.log('失敗', err)
})
// 成功 test
複製程式碼

再試試reject

let Promise = require('./myPromise')  // 引入模組
let p = new Promise(function(resolve, reject){
  reject('test')
})
p.then(function(data){
  console.log('成功', data)
},function(err){
  console.log('失敗', err)
})
// 失敗 test
複製程式碼

看起來不錯,但回撥函式是立即執行的,無法進行非同步操作,比如這樣是不行的

let p = new Promise(function(resolve, reject){
  setTimeout(function(){
    resolve(100)  
  }, 1000)
})
p.then(function(data){
  console.log('成功', data)
},function(err){
  console.log('失敗', err)
})
// 不會輸出任何程式碼
複製程式碼

原因是我們在then函式中只對成功態和失敗態進行了判斷,而例項被new時,執行器中的程式碼會立即執行,但setTimeout中的程式碼將稍後執行,也就是說,then方法執行時,Promise的狀態沒有被改變依然是pending態,所以我們要對pending態也做判斷,而由於程式碼可能是非同步的,那麼我們就要想辦法把回撥函式進行快取,並且,then方法是可以多次使用的,所以要能存多個回撥,那麼這裡我們用一個陣列。

  • 實現非同步

在例項上掛兩個引數

_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()
        })
    }
}
複製程式碼

現在可以執行非同步任務了,也可以多次then了,一個窮人版Promise就完成了,

  • 處理錯誤

上面的程式碼雖然能用,但經不起考驗,真正的Promise如果在例項中丟擲錯誤,應該走reject:

new Promise(function(resolve, reject){
  throw new Error('錯誤')
}).then(function(){
    
},function(err){
  console.log('錯誤:', err)  
})
// 錯誤: Error: 錯誤
複製程式碼

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

try{
    executor(resolve, reject)        
}catch(e){ // 如果捕獲發生異常,直接調失敗,並把引數穿進去
    reject(e)
}
複製程式碼
  • 實現then的鏈式呼叫(難點)

上面說過了,then可以鏈式呼叫,也是這一點讓Promise十分好用,當然這部分原始碼也比較複雜

我們知道jquery實現鏈式呼叫是return了一個this,但Promise不行,為什麼不行?

正宗的Promise是這樣的套路:

let p1 = new Promise(function(resolve, reject){
  resolve()
})
let p2 = p1.then(function(data){ //這是p1的成功回撥,此時p1是成功態
    throw new Error('錯誤') // 如果這裡丟擲錯誤,p2應是失敗態
})
p2.then(function(){
    
},function(err){
    console.log(err)
})
// Error: 錯誤
複製程式碼

如果返回的是this,那麼p2跟p1相同,固狀態也相同,但上面說了,Promise的成功態和失敗態不能相互轉換,那就不會得到p1成功而p2失敗的效果,而實際上是可能發生這種情況的。

所以Promise的then方法實現鏈式呼叫的原理是:返回一個新的Promise

在then方法中先定義一個新的Promise,取名為promise2(官方規定的),然後在三種狀態下分別用promise2包裝一下,在呼叫onFulfilled時用一個變數x(規定的)接收返回值,trycatch一下程式碼,沒錯就調resolve傳入x,有錯就調reject傳入錯誤,最後再把promise2給return出去,就可以進行鏈式呼叫了,,,,但是!

// 改動then
let promise2;
if (_this.status === 'resolved') {
    promise2 = new Promise(function (resolve, reject) {
        // 可以湊合用,但是是有很多問題的
        try { 
            let x = onFulfilled(_this.value)
            resolve(x)
        } catch (e) {
            reject(e)
        }
    })
}
if (_this.status === 'rejected') {
    promise2 = new Promise(function (resolve, reject) {
        // 可以湊合用,但是是有很多問題的
        try {
            let x = onRjected(_this.reason)
            resolve(x)
        } catch (e) {
            reject(e)
        }
    })
}
if(_this.status === 'pending'){
    promise2 = new Promise(function (resolve, rejec
        _this.onResolvedCallbacks.push(function(){
             // 可以湊合用,但是是有很多問題的
            try {
                let x = onFulfilled(_this.value)
                resolve(x)
            } catch (e) {
                reject(e)
            }
        })
        _this.onRejectedCallbacks.push(function(){
             // 可以湊合用,但是是有很多問題的
            try {
                let x = onRjected(_this.reason)
                resolve(x)
            } catch (e) {
                reject(e)
            }
        })
    })
}
return promise2
複製程式碼

這裡我先解釋一下x的作用再說為什麼不行,x是用來接收上一次then的返回值,比如這樣

let p = new Promise(function(resolve, reject){
  resolve(data)  
})
p.then(function(data){
    return xxx // 這裡返回一個值
}, function(){
    
}).then(function(data){
    console.log // 這裡會接收到xxx
}, function(){
    
})
// 以上程式碼中第一次then的返回值就是原始碼內第一次呼叫onRjected的返回值,可以用一個x來接收
複製程式碼

接下來說問題,上面這樣看起來是符合邏輯的,並且也確實可以鏈式呼叫並接受到,但我們在寫庫,庫就要經得起考驗,把容錯性提到最高,要接受使用者各種新(cao)奇(dan)操作,所謂有容nai大。可能性如下:

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,裡面啥也不寫。

。。

稍等,我先吐一會。。。

性感的Promise,擁抱ta然後扒光ta

好了我們們調整心情繼續擼,其實這一系列的問題,很多都是相關的,只要根據規範,都可以順利解決,接上面的程式碼,先幹三件事

1、問題7是最好解決的,如果沒傳resolve和reject,我們就給他一個。

2、官方規範規定了一件事

性感的Promise,擁抱ta然後扒光ta

簡單說就是為免在測試中出問題onFulfilled和onRejected要非同步執行,我們就讓他非同步執行

3、問題1-7,我們可以採取統一的覺得方案,定義一個函式來判斷和處理這一系列的情況,官方給出了一個叫做resolvePromise的函式

性感的Promise,擁抱ta然後扒光ta

再看then方法

Promise.prototype.then = function (onFulfilled, onRjected) {
    //成功和失敗預設不傳給一個函式,解決了問題8
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) {
        return value;
    }
    onRjected = typeof onRjected === 'function' ? onRjected : function (err) {
        throw err;
    }
    let _this = this;
    let promise2; //返回的promise
    if (_this.status === 'resolved') {
        promise2 = new Promise(function (resolve, reject) {
            // 當成功或者失敗執行時有異常那麼返回的promise應該處於失敗狀態
            setTimeout(function () {// 根據規範讓那倆傢伙非同步執行
                try {
                    let x = onFulfilled(_this.value);//這裡解釋過了
                    // 寫一個方法統一處理問題1-7
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (_this.status === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRjected(_this.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (_this.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {
            _this.onResolvedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onFulfilled(_this.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e)
                    }
                })
            });
            _this.onRejectedCallbacks.push(function () {
                setTimeout(function () {
                    try {
                        let x = onRjected(_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; // 表示是否呼叫過成功或者失敗,用來解決問題7
    //下面判斷上一次then返回的是普通值還是函式,來解決問題1、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') {
                // 成功
                //這裡的y也是官方規範,如果還是promise,可以當下一次的x使用
                //用call方法修改指標為x,否則this指向window
                then.call(x, function (y) {
                    if (called) return //如果呼叫過就return掉
                    called = true
                    // y可能還是一個promise,在去解析直到返回的是一個普通值
                    resolvePromise(promise2, y, resolve, reject)//遞迴呼叫,解決了問題6
                }, function (err) { //失敗
                    if (called) return
                    called = true
                    reject(err);
                })
            } else {
                resolve(x)
            }
        } catch (e) {
            if (called) return
            called = true;
            reject(e);
        }
    } else { // 說明是一個普通值1
        resolve(x); // 表示成功了
    }
}
複製程式碼
  • 測試一下

PromiseA+提供了測試庫promises-aplus-tests,github上明確講解了使用方法

性感的Promise,擁抱ta然後扒光ta
公開一個介面卡介面:

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

用命令列: promises-aplus-tests myPromise.js

性感的Promise,擁抱ta然後扒光ta

經過一系列測試得到結果

872 passing (18s)
複製程式碼

證明了我們的promise是完全符合規範的!

  • 其他方法

除了最重要的then方法,Promise還有很多方法,但都不難,這裡一次性介紹一遍

    // 捕獲錯誤的方法,在原型上有catch方法,返回一個沒有resolve的then結果即可
    Promise.prototype.catch = function (callback) {
        return this.then(null, callback)
    }
    // 解析全部方法,接收一個Promise陣列promises,返回新的Promise,遍歷陣列,都完成再resolve
    Promise.all = function (promises) {
        //promises是一個promise的陣列
        return new Promise(function (resolve, reject) {
            let arr = []; //arr是最終返回值的結果
            let i = 0; // 表示成功了多少次
            function processData(index, y) {
                arr[index] = y;
                if (++i === promises.length) {
                    resolve(arr);
                }
            }
            for (let i = 0; i < promises.length; i++) {
                promises[i].then(function (y) {
                    processData(i, y)
                }, reject)
            }
        })
    }
    // 只要有一個promise成功了 就算成功。如果第一個失敗了就失敗了
    Promise.race = function (promises) {
        return new Promise(function (resolve, reject) {
            for (var i = 0; i < promises.length; i++) {
                promises[i].then(resolve,reject)
            }
        })
    }
    // 生成一個成功的promise
    Promise.resolve = function(value){
        return new Promise(function(resolve,reject){
            resolve(value);
        })
    }
    // 生成一個失敗的promise
    Promise.reject = function(reason){
        return new Promise(function(resolve,reject){
            reject(reason);
        })
    }
複製程式碼

結語:Promise是非同步的較好的解決方案之一,通過對原始碼的解析,對Promise甚至js非同步都有了深刻的理解。Promise已經誕生很久了,如果你還不瞭解它,那你已經很落後了,抓緊時間上車。程式世界一日千里,作為程式設計師,要主動擁抱變化。

性感的Promise,擁抱ta然後扒光ta
歡迎加我的個人微信深入交流

相關文章