講講 Promise

一、什麼是 Promise

1.1 Promise 的前世今生

Promise 最早出現在 1988 年,由 Barbara LiskovLiuba Shrira 首創(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。並且在語言 MultiLispConcurrent Prolog 中已經有了類似的實現。

JavaScript 中,Promise 的流行是得益於 jQuery 的方法 jQuery.Deferred(),其他也有一些更精簡獨立的 Promise 庫,例如:QWhenBluebird

# Q / 2010
import Q from 'q'

function wantOdd () {
    const defer = Q.defer()
    const num = Math.floor(Math.random() * 10)
    if (num % 2) {
        defer.resolve(num)
    } else {
        defer.reject(num)
    }
    return defer.promise
}

wantOdd()
    .then(num => {
        log(`Success: ${num} is odd.`) // Success: 7 is odd.
    })
    .catch(num => {
        log(`Fail: ${num} is not odd.`)
    })

由於 jQuery 並沒有嚴格按照規範來制定接口,促使了官方對 Promise 的實現標準進行了一系列重要的澄清,該實現規範被命名爲 Promise/A+。後來 ES6(也叫 ES2015,2015 年 6 月正式發佈)也在 Promise/A+ 的標準上官方實現了一個 Promise 接口。

new Promise( function(resolve, reject) {...} /* 執行器 */  );

想要實現一個 Promise,必須要遵循如下規則:

  1. Promise 是一個提供符合標準then() 方法的對象。
  2. 初始狀態是 pending,能夠轉換成 fulfilledrejected 狀態。
  3. 一旦 fulfilledrejected 狀態確定,再也不能轉換成其他狀態。
  4. 一旦狀態確定,必須要返回一個值,並且這個值是不可修改的。

狀態

ECMAScript's Promise global is just one of many Promises/A+ implementations.

主流語言對於 Promise 的實現:Golang/go-promisePython/promiseC#/Real-Serious-Games/c-sharp-promisePHP/Guzzle PromisesJava/IOUObjective-C/PromiseKitSwift/FutureLibPerl/stevan/promises-perl

旨在解決的問題

由於 JavaScript 是單線程事件驅動的編程語言,通過回調函數管理多個任務。在快速迭代的開發中,因爲回調函數的濫用,很容易產生被人所詬病的回調地獄問題。Promise 的異步編程解決方案比回調函數更加合理,可讀性更強。

傳說中比較誇張的回調:

回調地獄

現實業務中依賴關係比較強的回調:

# 回調函數
function renderPage () {
    const secret = genSecret()
    // 獲取用戶令牌
    getUserToken({
        secret,
        success: token => {
            // 獲取遊戲列表
            getGameList({
                token,
                success: data => {
                    // 渲染遊戲列表
                    render({
                        list: data.list,
                        success: () => {
                            // 埋點數據上報
                            report()
                        },
                        fail: err => {
                            console.error(err)
                        }
                    })
                },
                fail: err => {
                    console.error(err)
                }
            })
        },
        fail: err => {
            console.error(err)
        }
    })
}

使用 Promise 梳理流程後:

# Promise
function renderPage () {
    const secret = genSecret()
    // 獲取用戶令牌
    getUserToken(token)
        .then(token => {
            // 獲取遊戲列表
            return getGameList(token)
        })
        .then(data => {
            // 渲染遊戲列表
            return render(data.list) 
        })
        .then(() => {
            // 埋點數據上報
            report()
        })
        .catch(err => {
            console.error(err)
        })
}

1.2 實現一個超簡易版的 Promise

Promise 的運轉實際上是一個觀察者模式,then() 中的匿名函數充當觀察者,Promise 實例充當被觀察者。

const p = new Promise(resolve => setTimeout(resolve.bind(null, 'from promise'), 3000))

p.then(console.log.bind(null, 1))
p.then(console.log.bind(null, 2))
p.then(console.log.bind(null, 3))
p.then(console.log.bind(null, 4))
p.then(console.log.bind(null, 5))
// 3 秒後
// 1 2 3 4 5 from promise

觀察者模式

# 實現
const defer = () => {
    let pending = [] // 充當狀態並收集觀察者
    let value = undefined
    return {
        resolve: (_value) => { // FulFilled!
            value = _value
            if (pending) {
                pending.forEach(callback => callback(value))
                pending = undefined
            }
        },
        then: (callback) => {
            if (pending) {
                pending.push(callback)
            } else {
                callback(value)
            }
        }
    }
}

# 模擬
const mockPromise = () => {
    let p = defer()
    setTimeout(() => {
        p.resolve('success!')
    }, 3000)
    return p
}

mockPromise().then(res => {
    console.log(res)
})

console.log('script end')
// script end
// 3 秒後
// success!

二、Promise 怎麼用

2.1 使用 Promise 異步編程

Promise 出現之前往往使用回調函數管理一些異步程序的狀態。

回調函數

# 常見的異步 Ajax 請求格式
ajax(url, successCallback, errorCallback)

Promise 出現後使用 then() 接收事件的狀態,且只會接收一次。

案例:插件初始化。

使用回調函數:

# 插件代碼
let ppInitStatus = false
let ppInitCallback = null
PP.init = callback => {
    if (ppInitStatus) {
        callback && callback(/* 數據 */)
    } else {
        ppInitCallback = callback
    }
}
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
ppInitCallback && ppInitCallback(/* 數據 */)
ppInitStatus = true

# 第三方調用
PP.init(callback)

使用 Promise:

# 插件代碼
let initOk = null
const ppInitStatus = new Promise(resolve => initOk = resolve)
PP.init = callback => {
    ppInitStatus.then(callback).catch(console.error)
}
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
initOk(/* 數據 */)

# 第三方調用
PP.init(callback)

相對於使用回調函數,邏輯更清晰,什麼時候初始化完成和觸發回調一目瞭然,不再需要重複判斷狀態和回調函數。當然更好的做法是隻給第三方輸出狀態數據,至於如何使用由第三方決定。

# 插件代碼
let initOk = null
PP.init = new Promise(resolve => initOk = resolve)
// ...
// ...
// 經歷了一系列同步異步程序後初始化完成
initOk(/* 數據 */)

# 第三方調用
PP.init.then(callback).catch(console.error)

2.2 鏈式調用

then() 必然返回一個 Promise 對象,Promise 對象又擁有一個 then() 方法,這正是 Promise 能夠鏈式調用的原因。

const p = new Promise(r => r(1))
    .then(res => {
        console.log(res) // 1
        return Promise.resolve(2)
        .then(res => res + 10) // === new Promise(r => r(1))
        .then(res => res + 10) // 由此可見,每次返回的是實例後面跟的最後一個 then
    })
    .then(res => {
        console.log(res) // 22
        return 3 // === Promise.resolve(3)
    })
    .then(res => {
        console.log(res) // 3
    })
    .then(res => {
        console.log(res) // undefined
        return '最強王者'
    })

p.then(console.log.bind(null, '是誰活到了最後:')) // 是誰活到了最後: 最強王者

由於返回一個 Promise 結構體永遠返回的是鏈式調用的最後一個 then(),所以在處理封裝好的 Promise 接口時沒必要在外面再包一層 Promise

# 包一層 Promise
function api () {
    return new Promise((resolve, reject) => {
        axios.get(/* 鏈接 */).then(data => {
            // ...
            // 經歷了一系列數據處理
            resolve(data.xxx)
        })
    })
}

# 更好的做法:利用鏈式調用
function api () {
    return axios.get(/* 鏈接 */).then(data => {
        // ...
        // 經歷了一系列數據處理
        return data.xxx
    })
}

2.3 管理多個 Promise 實例

Promise.all() / Promise.race() 可以將多個 Promise 實例包裝成一個 Promise 實例,在處理並行的、沒有依賴關係的請求時,能夠節約大量的時間。

function wait (ms) {
    return new Promise(resolve => setTimeout(resolve.bind(null, ms), ms))
}

# Promise.all
Promise.all([wait(2000), wait(4000), wait(3000)])
    .then(console.log)
// 4 秒後 [ 2000, 4000, 3000 ]

# Promise.race
Promise.race([wait(2000), wait(4000), wait(3000)])
    .then(console.log)
// 2 秒後 2000

2.4 Promiseasync / await

async / await 實際上只是建立在 Promise 之上的語法糖,讓異步代碼看上去更像同步代碼,所以 async / await 在 JavaScript 線程中是非阻塞的,但在當前函數作用域內具備阻塞性質。

let ok = null
async function foo () {
    console.log(1)
    console.log(await new Promise(resolve => ok = resolve))
    console.log(3)
}
foo() // 1
ok(2) // 2 3

使用 async / await 的優勢:

  1. 簡潔乾淨

    寫更少的代碼,不需要特地創建一個匿名函數,放入 then() 方法中等待一個響應。

    # Promise
    function getUserInfo () {
        return getData().then(
            data => {
                return data
            }
        )
    }
    
    # async / await
    async function getUserInfo () {
        return await getData()
    }
    
  2. 條件語句

    當一個異步返回值是另一段邏輯的判斷條件,鏈式調用將隨着層級的疊加變得更加複雜,讓人很容易在代碼中迷失自我。使用 async / await 將使代碼可讀性變得更好。

    # Promise
    function getGameInfo () {
        getUserAbValue().then(
            abValue => {
                if (abValue === 1) {
                    return getAInfo().then(
                        data => {
                            // ...
                        }
                    )
                } else {
                    return getBInfo().then(
                        data => {
                            // ...
                        }
                    )
                }
            }
        )
    }
    
    # async / await
    async function getGameInfo () {
        const abValue = await getUserAbValue()
        if (abValue === 1) {
            const data = await getAInfo()
            // ...
        } else {
            // ...
        }
    }
    
  3. 中間值

    異步函數常常存在一些異步返回值,作用僅限於成爲下一段邏輯的入場券,如果經歷層層鏈式調用,很容易成爲另一種形式的“回調地獄”。

    # Promise
    function getGameInfo () {
        getToken().then(
            token => {
                getLevel(token).then(
                    level => {
                        getInfo(token, level).then(
                            data => {
                                // ...
                            }
                        )
                    }
                )
            }
        )
    }
    
    # async / await
    async function getGameInfo() {
        const token = await getToken()
        const level = await getLevel(token)
        const data = await getInfo(token, level)
        // ...
    }
    
  4. 靠譜的 await

    await 'qtt' 等於 await Promise.resolve('qtt')await 會把任何不是 Promise 的值包裝成 Promise,看起來貌似沒有什麼用,但是在處理第三方接口的時候可以 “Hold” 住同步和異步返回值,否則對一個非 Promise 返回值使用 then() 鏈式調用則會報錯。

使用 async / await 的缺點:

  1. async 永遠返回 Promise 對象,不夠靈活,很多時候我只想單純返回一個基本類型值。

  2. await 阻塞 async 函數中的代碼執行,在上下文關聯性不強的代碼中略顯累贅。

    # async / await
    async function initGame () {
        render(await getGame()) // 等待獲取遊戲執行完畢再去獲取用戶信息
        report(await getUserInfo())
    }
    
    # Promise
    function initGame () {
        getGame()
            .then(render)
            .catch(console.error)
        getUserInfo() // 獲取用戶信息和獲取遊戲同步進行
            .then(report)
            .catch(console.error)
    }
    

2.5 錯誤處理

  1. 鏈式調用中儘量結尾跟 catch 捕獲錯誤,而不是第二個匿名函數。因爲標準裏註明了若 then() 方法裏面的參數不是函數則什麼都不錯,所以 catch(rejectionFn) 其實就是 then(null, rejectionFn) 的別名。

    anAsyncFn().then(
      resolveSuccess,
      rejectError
    )
    

    在以上代碼中,anAsyncFn() 拋出來的錯誤 rejectError 會正常接住,但是 resolveSuccess 拋出來的錯誤將無法捕獲,所以更好的做法是永遠使用 catch

    anAsyncFn()
      .then(resolveSuccess)
      .catch(rejectError)
    

    倘若講究一點,也可以通過 resolveSuccess 來捕獲 anAsyncFn() 的錯誤,catch 捕獲 resolveSuccess 的錯誤。

    anAsyncFn()
      .then(
        resolveSuccess,
        rejectError
      )
      .catch(handleError)
    
  2. 通過全局屬性監聽未被處理的 Promise 錯誤。

    瀏覽器環境(window)的拒絕狀態監聽事件:

    • unhandledrejection 當 Promise 被拒絕,並且沒有提供拒絕處理程序時,觸發該事件。
    • rejectionhandled 當 Promise 被拒絕時,若拒絕處理程序被調用,觸發該事件。
    // 初始化列表
    const unhandledRejections = new Map()
    // 監聽未處理拒絕狀態
    window.addEventListener('unhandledrejection', e => {
      unhandledRejections.set(e.promise, e.reason)
    })
    // 監聽已處理拒絕狀態
    window.addEventListener('rejectionhandled', e => {
      unhandledRejections.delete(e.promise)
    })
    // 循環處理拒絕狀態
    setInterval(() => {
      unhandledRejections.forEach((reason, promise) => {
        console.log('handle: ', reason.message)
        promise.catch(e => {
          console.log(`I catch u!`, e.message)
        })
      })
      unhandledRejections.clear()
    }, 5000)
    

注意:Promise.reject()new Promise((resolve, reject) => reject()) 這種方式不能直接觸發 unhandledrejection 事件,必須是滿足已經進行了 then() 鏈式調用的 Promise 對象纔行。

2.6 取消一個 Promise

當執行一個超級久的異步請求時,若超過了能夠忍受的最大時長,往往需要取消此次請求,但是 Promise 並沒有類似於 cancel() 的取消方法,想結束一個 Promise 只能通過 resolvereject 來改變其狀態,社區已經有了滿足此需求的開源庫 Speculation

或者利用 Promise.race() 的機制來同時注入一個會超時的異步函數,但是 Promise.race() 結束後主程序其實還在 pending 中,佔用的資源並沒有釋放。

Promise.race([anAsyncFn(), timeout(5000)])

2.7 迭代器的應用

若想按順序執行一堆異步程序,可使用 reduce。每次遍歷返回一個 Promise 對象,在下一輪 await 住從而依次執行。

function wasteTime (ms) {
    return new Promise(resolve => setTimeout(() => {
        resolve(ms)
        console.log('waste', ms)
    }, ms))
}

// 依次浪費 3 4 5 3 秒 === 15 秒
const arr = [3000, 4000, 5000, 3000]
arr.reduce(async (last, curr) => {
    await last
    return wasteTime(curr)
}, undefined)

三、總結

  1. 每當要使用異步代碼時,請考慮使用 Promise
  2. Promise 中所有方法的返回類型都是 Promise
  3. Promise 中的狀態改變是一次性的,建議在 reject() 方法中傳遞 Error 對象。
  4. 確保爲所有的 Promise 添加 then()catch() 方法。
  5. 使用 Promise.all() 行運行多個 Promise
  6. 倘若想在 then()catch() 後都做點什麼,可使用 finally()
  7. 可以將多個 then() 掛載在同一個 Promise 上。
  8. async (異步)函數返回一個 Promise,所有返回 Promise 的函數也可以被視作一個異步函數。
  9. await 用於調用異步函數,直到其狀態改變(fulfilled or rejected)。
  10. 使用 async / await 時要考慮上下文的依賴性,避免造成不必要的阻塞。

更多文章訪問我的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章