Promise規範與原理解析

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/11/24/17853279.html
-Advertisement-
Play Games

Promise對象用於清晰的處理非同步任務的完成,返回最終的結果值,本次分享主要介紹Promise的基本屬性以及Promise內部的基礎實現,能夠幫我們更明確使用場景、更快速定位問題。 ...


摘要

Promise對象用於清晰的處理非同步任務的完成,返回最終的結果值,本次分享主要介紹Promise的基本屬性以及Promise內部的基礎實現,能夠幫我們更明確使用場景、更快速定位問題。

Promise出現的原因

首先我們先來看一段代碼:非同步請求的層層嵌套

function fn1(params) {
  const xmlHttp = new XMLHttpRequest();
  xmlHttp.onreadystatechange = function(){
    if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
      const fn1Data = {name: 'fn1'}
      console.log(fn1Data, 'fn1Data');
      // 請求2
      (function fn2() {
        xmlHttp.onreadystatechange = function(){
        if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
          const fn2Data = {name: `${fn1Data.name}-fn2`}
          console.log(fn2Data, 'fn2Data');
          // 請求3
          (function fn2() {
            xmlHttp.onreadystatechange = function(){
            if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
              const fn3Data = {name: `${fn2Data.name}-fn3`}
              console.log(fn3Data, 'fn3Data');
            }
          }
          xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true);
          xmlHttp.send();
          })()
        }
      }
      xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true);
      xmlHttp.send();
      })()
    }
  }
  xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true);
  xmlHttp.send();
}

fn1()



或者我們可以將上面的代碼優化為下麵這樣

function fn1(params) {
  console.log(`我是fn1,我在函數${params}中執行!!!`);
}
  
function fn2(params) {
  try {
    const xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function(){
      if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
        console.log(`我是fn2,我在函數${params}中執行!!!結果是:`,params.data);
        fn1('fn2')
      }
    }
    xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true);
    xmlHttp.send();
  } catch (error) {
    console.error(error);
  }
}
  
function fn3() {
  try {
    const xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function(){
      if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
          console.log('fn3請求已完成');
          fn2('fn3')
      }
    }
    xmlHttp.open("GET","https://v0.yiketianqi.com/api?unescape=1&version=v61", true);
    xmlHttp.send();
    console.log('我是f3函數呀');
  } catch (error) {
    console.error(error);
  }
}
  
fn3()



由上面的兩種寫法的請求可見,在promise之前,為了進行多個非同步請求並且依賴上一個非同步請求的結果時,我們必須進行層層嵌套,大多數情況下,我們又對非同步結果進行數據處理,這樣使得我們的代碼非常難看,並且難以維護,這就形成了回調地獄,由此Promise開始出現了。

回調地獄缺點

  • 代碼臃腫
  • 可讀性差
  • 耦合性高
  • 不好進行異常處理

Promise的基本概念

含義

  1. ES6將其寫進了語言標準里統一了用法,是一個構造函數,用來生成Promise實例
  2. 參數為一個執行器函數(執行器函數是立即執行的),該函數有兩個函數作為參數,第一個參數是成功時的回調,第二個參數是失敗時的回調
  3. 函數的方法有resolve(可以處理成功和失敗)、reject(只處理失敗)、all等方法
  4. then、catch、finally方法為Promise實例上的方法

狀態

  1. pending --- 等待狀態
  2. Fulfilled --- 執行狀態 (resolve回調函數,then)
  3. Rejected --- 拒絕狀態 (reject回調函數,catch)
  4. 狀態一旦改變就不會再變,狀態只可能是兩種改變,從pending->Fulfilled,pending->Rejected
  5. 有兩個關鍵的屬性:PromiseState --- 狀態改變,PromiseResult --- 結果數據改變
const p1 = Promise.resolve(64)
const p2 = Promise.reject('我錯了')
const p3 = Promise.then()
const p4 = Promise.catch()

// 狀態改變PromiseState 結果改變PromiseResult
console.log(new Promise(()=>{}), 'Promise');  // PromiseState='pending' PromiseResult=undefined
console.log(p1,'p1');  // PromiseState='Fulfilled' PromiseResult=64
console.log(p2,'p2');  // PromiseState="Rejected" PromiseResult='我錯了'
console.log(p3, 'p3'); // then為實例上的方法,報錯
console.log(p4, 'p4');  // catch為實例上的方法,報錯


特點

  1. 錯誤信息清晰定位:可以在外層捕獲異常信息(網路錯誤、語法錯誤都可以捕獲),有“冒泡”性質,會一直向後傳遞,直到被捕獲,所以在最後寫一個catch就可以了

  2. 鏈式調用:每一個then和catch都會返回一個新的Promise,把結果傳遞到下一個then/catch中,因此可以進行鏈式調用 --- 代碼簡潔清晰

結果由什麼決定

resolve

  1. 如果傳遞的參數是非Promise類型的對象,則返回的結果是成功狀態的Promise對象,進入下一個then裡面
  2. 如果傳遞的參數是Promise類型的對象,則返回的結果由返回的Promise決定,如果返回的是resolve則是成功的狀態,進入下一個then里,如果返回的是reject則是失敗的狀態,進入下一個catch里

reject

  1. 如果傳遞的參數是非Promise類型的對象,則返回的結果是拒絕狀態的Promise對象,進入下一個catch裡面或者是下一個then的第二個參數reject回調裡面
  2. 如果傳遞的參數是Promise類型的對象,則返回的結果由返回的Promise決定,如果返回的是resolve則是成功的狀態,進入下一個then里,如果返回的是reject則是拒絕的狀態,進入下一個catch裡面或者是下一個then的第二個參數reject回調裡面

這在我們自己封裝的API裡面也有體現:為什麼code為1時都是then接收,其他都是catch接收,就是因為在then裡面也就是resolve函數中對code碼進行了判斷,如果是1則返回Promise.resolve(),進入then里處理,如果是非1則返回Promise.reject(),進入catch里處理。

流程圖

簡單使用

// 模擬一個promise的get請求
let count = 0
function customGet(url){
    count += 1
    return new Promise((resolve, reject)=>{
        const xmlHttp = new XMLHttpRequest();
        xmlHttp.open("GET",url, true);
        xmlHttp.onload = ()=>{
          console.log(xmlHttp, 'xmlHttp---onload');
          if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
            console.log('customGet請求成功了');
            // 返回非Promise,結果為成功狀態
            resolve({data:`第${count}次請求獲取數據成功`})

            // 返回Promise,結果由Promise決定
            // resolve(Promise.reject('resolve中返回reject'))
          } else {
            reject('customGet請求錯誤了')
          }
        }

        // Promise狀態改變就不會再變
        // onreadystatechange方法會被執行四次
        // 當地次進來的時候,readyState不等於4,執行else邏輯,執行reject,狀態變為Rejected,所以即使再執行if,狀態之後不會再改變
        // xmlHttp.onreadystatechange = function(){
        //   console.log(xmlHttp,'xmlHttp---onreadystatechange')
        //   if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
        //     console.log('customGet請求成功了');
        //     resolve({data:`第${count}次請求獲取數據成功`})
        //   } else {
        //     reject('customGet請求錯誤了')
        //   }
        // }
        xmlHttp.send();
      })
 }

// 使用Promise,並且進行鏈式調用
customGet('https://v0.yiketianqi.com/api/cityall?appid=&appsecret=').then((res)=>{
   console.log(res.data);
   return '第一次請求處理後的數據'
}).then((data)=>{
   console.log(data)
   // console.log(data.toFixed());
   return customGet('https://v0.yiketianqi.com/api/cityall?appid=&appsecret=')
}).then((res)=>{
   console.log(res.data);
}).catch((err)=>{
    // 以類似'冒泡'的性質再外層捕獲所有的錯誤
   console.error(err, '這是catch里的錯誤信息');
})



手寫實現簡單的Promise

通過上面的回顧,我們已經瞭解了Promise的關鍵屬性和特點,下麵我們一起來實現一個簡單的Promise吧

  // 1、封裝一個Promise構造函數,有一個函數參數
  function Promise(executor){
    // 7、添加對象屬性PromiseState PromiseResult
    this.PromiseState = 'pending'
    this.PromiseResult = null

    // 14、創建一個保存成功失敗回調函數的屬性
    this.callback = null

    // 8、this指向問題
    const that = this

    // 4、executor有兩個函數參數(resolve,reject)
    function resolve(data){
      // 10、Promise狀態只能修改一次(同時記得處理reject中的狀態)
      if(that.PromiseState !== 'pending') return

      // console.log(this, 'this');
      // 5、修改對象的狀態PromiseState
      that.PromiseState = 'Fulfilled'

      // 6、修改對象的結果PromiseResult
      that.PromiseResult = data

      // 15、非同步執行then里的回調函數
      if(that.callback?.onResolve){
        that.callback.onResolve(that.PromiseResult)
      }
    }
    function reject(data){
      console.log(that.PromiseState, 'that.PromiseState');
      if(that.PromiseState !== 'pending') return

      // 9、處理失敗函數狀態
      that.PromiseState = 'Rejected'
      that.PromiseResult = data
      console.log(that.PromiseResult, 'that.PromiseResult');
      console.log(that.PromiseState, 'that.PromiseState');

      // 16、非同步執行then里的回調函數
      if(that.callback?.onReject){
        that.callback.onReject(that.PromiseResult)
      }
    }
    // 3、執行器函數是同步調用的,並且有兩個函數參數
    executor(resolve,reject)
  }
  // 2、函數的實例上有方法then
  Promise.prototype.then = function(onResolve,onReject){
    // 20、處理onReject沒有的情況
    if(typeof onReject !== 'function'){
      onReject = reason => {
        throw reason
      }
    }
    // 21、處理onResolve沒有的情況
    if(typeof onResolve !== 'function'){
      onResolve = value => value
    }
    // 17、每一個then方法都返回一個新的Promise,並且把上一個then返回的結果傳遞出去
    return new Promise((nextResolve,nextReject)=>{
      // 11、處理成功或失敗
      if(this.PromiseState === 'Fulfilled'){
        // 12、將結果傳遞給函數
        // onResolve(this.PromiseResult)

        // 18、拿到上一次執行完後返回的結果,判斷是不是Promise
        const result = onResolve(this.PromiseResult)
        if(result instanceof Promise){
          result.then((v)=>{
            nextResolve(v)
          },(r)=>{
            nextReject(r)
          })
        } else {
          nextResolve(result)
        }
      }
      // 當你一步步寫下來的時候有沒有懷疑過為什麼不用else
       if(this.PromiseState === 'Rejected'){
            // 第12步同時處理此邏輯
            // onReject(this.PromiseResult)

            // 22、處理catch異常穿透捕獲錯誤
            try {
              const result = onReject(this.PromiseResult)
              if(result instanceof Promise){
                result.then((v)=>{
                  nextResolve(v)
                }).catch((r)=>{
                  nextReject(r)
                })
              } else {
                nextReject(result)
              }
            } catch (error) {
              nextReject(this.PromiseResult)
            }
         }
  
      // 13、非同步任務時處理成功或失敗,想辦法等非同步任務執行完成後才去執行這兩個函數
      if(this.PromiseState === 'pending'){
        this.callback = {
          onResolve,
          onReject
        }
        console.log(this.callback, 'this.callback');
      }
    })
  }
  // 19、函數實例上有方法catch
  Promise.prototype.catch = function(onReject) {
    return this.then(null,onReject)
  }

  // 使用自定義封裝的Promise
  const customP = new Promise((resolve,reject)=>{
    // 模擬非同步執行請求
    // const xmlHttp = new XMLHttpRequest();
    // xmlHttp.open("GET",'https://v0.yiketianqi.com/api/cityall?appid=&appsecret=', true);
    // xmlHttp.onload = ()=>{
    //   if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
    //     resolve('success')
    //   } else {
    //     reject('error')
    //   }
    // }
    // xmlHttp.send();

    // 同步執行
    resolve('success')
    // reject('error')
  })

  console.log(customP, 'customP');
  customP.then((res)=>{
    console.log(res, 'resolve回調');
    return '第一次回調'
    // return new Promise((resolve,reject)=>{
    //   reject('錯錯錯')
    // })
  },(err)=>{
    console.error(err, 'reject回調');
    return '2121'
  }).then(()=>{
    console.log('then裡面輸出');
  }).then().catch((err)=>{
    console.error(err, 'catch里的錯誤');
  })



針對resolve中返回Promise對象時的內部執行順序

總結

以上就是我們常用的Promise基礎實現,在實現過程中對比了Promise和函數嵌套處理非同步請求的優缺點,Promise仍存在缺點,但是的確方便很多,同時更清晰的理解到錯誤處理如何進行異常穿透的,也能幫助我們更規範的使用Promise以及快速定位問題所在。

作者:京東物流 孫琦

來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源


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

-Advertisement-
Play Games
更多相關文章
  • 1. SSMS 方式 在資料庫中選擇“編輯前200行”選項,然後就可以手動直接輸入數據到表行中了。 手動輸入的數據是確定的,而且只能一點點輸入,遇到大量的數據的時候,操作會變得很繁重,而且它不滿足大多數業務的需求,而且不利於更新。 在對象資源管理器中,右鍵點擊你要打開的表,然後選擇 “Edit To ...
  • 企業業務一直依賴於其收集的數據,但這些數據集正在不斷增長。即使需要提取的數據存儲量非常龐大,Redis Enterprise 7.2也能使應用程式能以最快的速度檢索和處理數據。利用自動分層技術,可以識別冷數據和熱數據,並分別存儲在記憶體和固態硬碟上,顯著提升了系統響應速度,優化記憶體占用的同時降低硬體成... ...
  • 本文是對這篇文章MySQL InnoDB Cluster - Navigating the Cluster[1]的翻譯,翻譯如有不當的地方,敬請諒解,請尊重原創和翻譯勞動成果,轉載的時候請註明出處。謝謝! 當我們管理InnoDB Cluster時,一件非常重要的事情就是瞭解集群處於什麼樣的狀態,特別 ...
  • 副本集概述 副本集(Replica Set)是一組帶有故障轉移的 MongoDB 實例組成的集群,由一個主(Primary)伺服器和多個從(Secondary)伺服器構成。通過Replication,將數據的更新由Primary推送到其他實例上,在一定的延遲之後,每個MongoDB實例維護相同的數據 ...
  • 最近接觸到了一個問題:耳機插入事件的由來,走讀了下IMS輸入系統服務的源碼。同時,IMS輸入系統服務在Android的開發過程中,也經常出現,有必要瞭解下相關原理。 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 最近喜歡研究起了手錶,對勞力士這款“百事圈”實在是心水的不行啊! 心癢難耐無奈錢包不支持,作為一個前端程式員,買不起的東西該怎麼辦? 當然是自己做一個啊! 說乾就乾,熬夜自己做了個“百事圈”出來!源碼在最後! 先看成品 還是有那麼六七成相 ...
  • 沒有什麼是不可能的,只是需要找到正確的方法。 1. 什麼是狀態? 狀態就是組件內部特有數據的載體(組件數據掛載方式),改變數據頁面就會刷新,由組件自己設置和更改,也就是說由組件自己產生、維護,使用狀態的目的就是為了在不同的狀態下使組件的顯示不同(自己管理),這在 React 中稱為:條件渲染。 為什 ...
  • Node.js是一個基於ChromeV8引擎的JavaScript運行環境,使用了一個事件驅動、非阻塞式I/O模型,讓JavaScript 運行在服務端的開發平臺,它讓JavaScript成為與PHP、Python、Perl、Ruby等服務端語言平起平坐的腳本語言。Node中增添了很多內置的模塊,提... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...