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 转载请注明来源

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax