写点什么

Promise 规范与原理解析 | 京东物流技术团队

  • 2023-11-24
    北京
  • 本文字数:5841 字

    阅读完需:约 19 分钟

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 结果改变PromiseResultconsole.log(new Promise(()=>{}), 'Promise'); // PromiseState='pending' PromiseResult=undefinedconsole.log(p1,'p1'); // PromiseState='Fulfilled' PromiseResult=64console.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 = 0function 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
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Promise规范与原理解析 | 京东物流技术团队_前端_京东科技开发者_InfoQ写作社区