講講 Promise

1、什麼是 Promise

1.1 Promise 的前世此生

Promise 最先出如今 1988 年,由 Barbara LiskovLiuba Shrira 獨創(論文:Promises: Linguistic Support for Efficient Asynchronous Procedure Calls in Distributed Systems)。而且在語言 MultiLispConcurrent Prolog 中已經有了相似的實現。html

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

# 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 接口。jquery

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

想要實現一個 Promise,必需要遵循以下規則:ios

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

狀態

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

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

旨在解決的問題

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

傳說中比較誇張的回調:編程

回調地獄

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

# 回調函數
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 梳理流程後:api

# 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!
複製代碼

2、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)
複製代碼

3、總結

  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 時要考慮上下文的依賴性,避免形成沒必要要的阻塞。

更多文章訪問個人博客

相關文章
相關標籤/搜索