Promise這個折磨人的小妖精

彭道寬發表於2018-07-28

前言

不得不說, promise 這玩意,是每個面試官都會問的問題,但是你真的瞭解promise嗎?其實我也不瞭解,下面的內容都是我從掘金、知乎、《ECMAScript6入門》上看的部落格文章等資料,然後總結的,畢竟自己寫一遍,更有助於理解,如有錯誤,請指出 ~

什麼是回撥地獄 ?

在過去寫非同步程式碼都要靠回撥函式,當非同步操作依賴於其他非同步操作的返回值時,會出現一種現象,被程式設計師稱為 “回撥地獄”,比如這樣 :

    // 假設我們要請求使用者資料資訊,它接收兩個回撥,假設我們要請求使用者資料資訊,它接收兩個回撥,successCallback 和 errCallback

    function getUserInfo (successCallback, errCallback) {
        $.ajax({
            url : 'xxx',
            method : 'get',
            data : {
                user_id : '123'
            },
            success : function(res) {
                successCallback(res)    // 請求成功,執行successCallback()回撥
            },
            error : function(err) {
                errCallback(err)        // 請求失敗,執行errCallback()回撥
            }
        })
    }
複製程式碼

騙我 ? 這哪裡複雜了,明明很簡單啊,說好的回撥地獄呢 ? 不急,繼續看

假設我們拿到了使用者資訊,但是我們還要拿到該使用者的聊天列表,然後再拿到跟某一“陌生”男人的聊天記錄呢 ?

    // getUserInfo -> getConnectList -> getOneManConnect()

    getUserInfo((res)=>{
        getConnectList(res.user_id, (list)=>{
            getOneManConnect(list.one_man_id, (message)=>{
                console.log('這是我和某位老男人的聊天記錄')
            }, (msg_err)=>{
                console.log('獲取詳情失敗,別汙衊我,我不跟老男人聊天')
            })
        }, (list_err)=>{
            console.log('獲取列表失敗,我都不跟別人聊天')
        })
    }, (user_err)=>{
        console.log('獲取使用者個人資訊失敗')
    })
複製程式碼

大兄弟,刺激不,三層巢狀,再多來幾個巢狀,就是 “回撥地獄” 了。這時候,promise來了。

Promise 簡介

阮一峰老師的《ECMAScript 6入門》裡對promise的含義是 : Promise 是非同步程式設計的一種解決方案,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。從語法上說,Promise 是一個物件,從它可以獲取非同步操作的訊息。Promise 提供統一的 API,各種非同步操作都可以用同樣的方法進行處理。

簡單來說,Promise就是對非同步的執行結果的描述物件。

狀態

  • pending (進行中)
  • fulfilled (已成功)
  • rejected (已失敗)
    1 : 只有非同步操作的結果,可以決定當前是哪一種狀態,任何其他操作都無法改變這個狀態。
    2 : 一旦狀態改變,就不會再變,任何時候都可以得到這個結果。
    3 : Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected
複製程式碼

知乎形象例子來說明promise

// 定外賣就是一個Promise,Promise的意思就是承諾
// 我們定完外賣,飯不會立即到我們手中
// 這時候我們和商家就要達成一個承諾
// 在未來,不管飯是做好了還是燒糊了,都會給我們一個答覆
function 定外賣(){
    // Promise 接受兩個引數
    // resolve: 非同步事件成功時呼叫(菜燒好了)
    // reject: 非同步事件失敗時呼叫(菜燒糊了)
    return new Promise((resolve, reject) => {
        let result = 做飯()
	// 下面商家給出承諾,不管燒沒燒好,都會告訴你
	if (result == '菜燒好了') 
	    // 商家給出了反饋
	    resolve('我們的外賣正在給您派送了')
	else 
	    reject('不好意思,我們菜燒糊了,您再等一會')
	})
}

// 商家廚房做飯,模擬概率事件
function 做飯() {
    return Math.random() > 0.5 ? '菜燒好了' : '菜燒糊了'
}

// 你在家上餓了麼定外賣
// 有一半的概率會把你的飯燒糊了
// 好在有承諾,他還是會告訴你

定外賣()
    // 菜燒好執行,返回'我們的外賣正在給您派送了'
    .then(res => console.log(res))
    // 菜燒糊了執行,返回'不好意思,我們菜燒糊了,您再等一會'
    .catch(res => console.log(res))

複製程式碼

基本用法

Promise 物件是一個建構函式,用來生成一個Promise例項。

    Promise建構函式接受一個函式作為引數,這個函式有兩個引數,分別是resolve()和reject()。

    resovle()函式是將Promise物件從pending變成fulfilled,在非同步操作完成時執行,將非同步結果,作為引數傳遞出去。

    reject()函式是將Promise物件從pending變成rejected,在非同步執行失敗時執行,將報錯資訊,作為引數傳遞出去。

    // 簡單的一個promise例項, 來自阮一峰老師的es6 示例程式碼
    const promise = new Promise((resolve, reject) => {
        // some code 

        if(/* 非同步執行成功 */) {
            resolve(res)
        } else {
            reject(error)
        }
    })
複製程式碼

then方法

Promise 有個.then()方法,then 方法中的回撥在微任務佇列中執行,支援傳入兩個引數,一個是成功的回撥,一個是失敗的回撥,在 Promise 中呼叫了 resolve 方法,就會在 then 中執行成功的回撥,呼叫了 reject 方法,就會在 then 中執行失敗的回撥,成功的回撥和失敗的回撥只能執行一個,resolve 和 reject 方法呼叫時傳入的引數會傳遞給 then 方法中對應的回撥函式。

    // 執行 resolve  
    let promise = new Promise((resolve, reject) => {
        console.log(1)
        resolve(3)
    })

    console.log(2)

    promise.then((data)=>{
        console.log(data)
    }, (err)=>{
        console.log(err)
    })

    // 1
    // 2
    // 3
複製程式碼
    // 執行 reject  
    let promise = new Promise((resolve, reject) => {
        console.log(1)
        reject()
    })

    promise.then(()=>{
        console.log(2)
    }, ()=>{
        console.log(3)
    })

    // 1
    // 3
複製程式碼

then方法

[ 注意 : then方法中的回撥是非同步的!!!]

為什麼上面第一個示例程式碼的結果是 1 -> 2 -> 3呢 ?傳入Promise 中的執行函式是立即執行完的啊,為什麼不是立即執行 then 中的回撥呢?因為then 中的回撥是非同步執行,表示該回撥是插入事件佇列末尾,在當前的同步任務結束之後,下次事件迴圈開始時執行佇列中的任務。

Promise 的回撥函式不是正常的非同步任務,而是微任務(microtask)。它們的區別在於,正常任務追加到下一輪事件迴圈,微任務追加到本輪事件迴圈。這意味著,微任務的執行時間一定早於正常任務

then方法的返回值是一個新的GPromise物件,這就是為什麼promise能夠進行鏈式操作的原因。

then方法中的一個難點就是處理非同步,通過setInterval來監聽GPromise物件的狀態改變,一旦改變,就是執行GPromise對應的then方法中相應的回撥函式。這樣回撥函式就能夠插入事件佇列末尾,非同步執行。
複製程式碼
    then有兩個引數 : onFulfilled 和 onRejected
    
    · 當狀態state為fulfilled,則執行onFulfilled,傳入this.value。當狀態state為rejected,則執行onRejected,傳入this.reason

    · onFulfilled,onRejected如果他們是函式,則必須分別在fulfilled,rejected後被呼叫,value或reason依次作為他們的第一個引數

    class Promise{
        constructor(executor){...}
        // then 方法 有兩個引數onFulfilled onRejected
        then(onFulfilled,onRejected) {
            // 狀態為fulfilled,執行onFulfilled,傳入成功的值
            if (this.state === 'fulfilled') {
                onFulfilled(this.value);
            };
            // 狀態為rejected,執行onRejected,傳入失敗的原因
            if (this.state === 'rejected') {
                onRejected(this.reason);
            };
        }
    }

複製程式碼

Promise的鏈式呼叫

由於promise每次呼叫then方法就會返回一個新的promise物件,如果該then方法中執行的回撥函式有返回值,那麼這個返回值就會作為下一個promise例項的then方法回撥的引數,如果 then 方法的返回值是一個 Promise 例項,那就返回一個新的 Promise 例項,將 then 返回的 Promise 例項執行後的結果作為返回 Promise 例項回撥的引數。

還記得剛開頭說的那個“陌生”男人例子嗎 ?這裡我們用promise的鏈式操作重寫下

    // 原來的程式碼
    getUserInfo((res)=>{
        getConnectList(res.user_id, (list)=>{
            getOneManConnect(list.one_man_id, (message)=>{
                console.log('這是我和某位老男人的聊天記錄')
            }, (msg_err)=>{
                console.log('獲取詳情失敗,別汙衊我,我不跟老男人聊天')
            })
        }, (list_err)=>{
            console.log('獲取列表失敗,我都不跟別人聊天')
        })
    }, (user_err)=>{
        console.log('獲取使用者個人資訊失敗')
    })

    
    // Promise重寫的程式碼
    function handleAjax (params) {
        return new Promise((resolve, reject)=>{
            $.ajax({
                url : params.url,
                type : params.type || 'get',
                data : params.data || '',
                success : function(data) {
                    resolve(data)
                },
                error : function(error) {
                    reject(error)
                }
            })
        })
    }

    const promise = handleAjax({
        url : 'xxxx/user'
    });

    promise.then((data1)=>{
        console.log('獲取個人資訊成功')       // 獲取個人資訊成功
        return handleAjax({
            url : 'xxxx/user/connectlist',
            data : data1.user_id
        });
    })
    .then((data2)=>{
        console.log('獲得聊天列表')
        return handleAjax({
            url : 'xxxx/user/connectlist/one_man',
            data : data2.one_man_id
        });
    })
    .then((data3)=>{
        console.log('獲得跟某男人的聊天')
    })
    .catch((err)=>{
        console.log(err)
    }) 
複製程式碼

來自ES6的 Promise.prototype.then()

Promise 例項具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的。它的作用是為 Promise 例項新增狀態改變時的回撥函式。前面說過,then方法的第一個引數是resolved狀態的回撥函式,第二個引數(可選)是rejected狀態的回撥函式。

then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。

採用鏈式的then,可以指定一組按照次序呼叫的回撥函式。這時,前一個回撥函式,有可能返回的還是一個Promise物件(即有非同步操作),這時後一個回撥函式,就會等待該Promise物件的狀態發生變化,才會被呼叫

來自ES6的 Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。Promise物件狀態變為resolved,則會呼叫then方法指定的回撥函式;如果非同步操作丟擲錯誤,狀態就會變為rejected,就會呼叫catch方法指定的回撥函式,處理這個錯誤。另外,then方法指定的回撥函式,如果執行中丟擲錯誤,也會被catch方法捕獲。

Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲

一般來說,不要在then方法裡面定義 reject 狀態的回撥函式(即then的第二個引數),總是使用catch方法。

來自ES6的 Promise.all()

Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。

    const p = Promise.all([p1, p2, p3])
複製程式碼

Promise.all方法接受一個陣列作為引數,p1、p2、p3都是 Promise 例項,如果不是,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。

p的狀態由p1、p2、p3決定,分成兩種情況。

(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。

(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
複製程式碼

來自ES6 的Promise.race()

Promise.race方法同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。

    const p = Promise.all([p1, p2, p3])
複製程式碼

上面程式碼中,只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式

Promise.race方法的引數與Promise.all方法一樣,如果不是 Promise 例項,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。

來自ES6 的Promise.resolve()

有時需要將現有物件轉為 Promise 物件,Promise.resolve方法就起到這個作用

    Promise.resolve('test')
    // 等價於
    new Promise(resolve => resolve('test'))

    // 更多請看阮一峰老師的ES6 Promise物件
複製程式碼

來自ES6 的Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 例項,該例項的狀態為rejected。

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

    p.then(null, function (err) {
        console.log(err)    // 出錯了
    });
    
    // 更多請看阮一峰老師的ES6 Promise物件
複製程式碼

相關連結

阮一峰 ES6 : es6.ruanyifeng.com/#docs/promi…

知乎例子 : zhuanlan.zhihu.com/p/29632791

掘金 卡姆愛卡姆 : juejin.im/post/5b2f02…

來自segmentfault 的GEEK作者 : segmentfault.com/a/119000001…

個人部落格 : blog.pengdaokuan.cn:4001

相關文章