講講 Promise

来源:https://www.cnblogs.com/mazey/archive/2020/07/03/13232913.html
-Advertisement-
Play Games

一、什麼是 Promise 1.1 Promise 的前世今生 Promise 最早出現在 1988 年,由 Barbara Liskov、Liuba Shrira 首創(論文:Promises: Linguistic Support for Efficient Asynchronous Proce ...


一、什麼是 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 時要考慮上下文的依賴性,避免造成不必要的阻塞。

更多文章訪問我的博客


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文更新於2019-06-22,使用MySQL 5.7,操作系統為Deepin 15.4。 為了便於描述,此處將創建視圖的DDL覆述一次,其已於“SQL”章節描述。 CREATE [OR REPLACE] [ALGORITHM={UNDEFINED|MERGE|TEMPTABLE}] VIEW vi ...
  • Plink是一個基於Flink的流處理平臺,旨在基於 [Apache Flink]封裝構建上層平臺。 提供常見的作業管理功能。如作業的創建,刪除,編輯,更新,保存,啟動,停止,重啟,管理,多作業模板配置等。 Flink SQL 編輯提交功能。如 SQL 的線上開發,智能提示,格式化,語法校驗,保存, ...
  • 前言 閑暇之時,羚羊給大家分享一下羚羊在Centos7 下安裝Cloudera Manager 6.3.0和cloudera cdh 6.3.2的過程和安裝過程中遇到的坑。至於為什麼要選擇CDH,Cloudera Manager和cdh是什麼,之間又是什麼關係,在這裡羚羊就不做介紹了。 為什麼選擇C ...
  • 零基礎入門貪吃蛇游戲 貪吃蛇是一款最常見、最經典、最受歡迎的小游戲之一。本篇文章帶你零基礎實現貪吃蛇游戲,一條蛇的使命從這裡開始。 演示地址:貪吃蛇演示,可能會提示危險操作,請忽略,放心訪問。 1、游戲描述 貪吃蛇是一款非常經典的休閑類游戲。在一塊固定大小的區域內,游戲玩家通過控制貪吃蛇的移動去吃食 ...
  • 前言: 在多線程中線程的執行順序是依靠哪個線程先獲得到CUP的執行權誰就先執行,雖然說可以通過線程的優先權進行設置,但是他只是獲取CUP執行權的概率高點,但是也不一定必須先執行。在這種情況下如何保證線程按照一定的順序進行執行,今天就來一個大總結,分別介紹一下幾種方式。 一、通過Object的wait ...
  • 如需轉載,請註明出處:Flutter學習筆記(16)--BottomNavigationBar底部item超過3個只顯示icon,不顯示title items: [ _bottomItem(Ids.home, 'ic_home_normal', 'ic_home_selected', 0), _bo ...
  • 什麼是 React 高階組件 React 高階組件就是以高階函數的方式包裹需要修飾的 React 組件,並返回處理完成後的 React 組件。React 高階組件在 React 生態中使用的非常頻繁,比如react-router 中的 withRouter 以及 react-redux 中 conn ...
  • 新的一天又開始了,今天學習了三個內容 一、表格標簽 table 在各個網頁中我們會發現許多商品頁面,展示是用什麼來展示的呢,表格無疑是一種不錯的選擇看下圖。 一張常見的簡歷就是有表格標簽來編寫的 創建表格: <!-- 表格由基本標簽table tr td組成其中tr代表表格的行 td代表表格的列,行 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...