如何深入理解异步编程的核心 Promise?

如何深入理解异步编程的核心 Promise?

  其实在 ES6标准出现之前,社区就最早提出了 Promise的方案,后随着 ES6将其加入进去,才统一了其用法,并提供了原生的 Promise对象。Promise也是日常前端开发使用比较多的编程方式,因此需要对 Promise 异步编程的思路有更深刻的理解。

  先抛出几个问题:

  1. Promise内部究竟有几种状态?
  2. Promise是怎么解决回调地狱问题的?

Promise 的基本情况

  如果一定要解释 Promise 到底是什么,简单来说它就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

  Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。我们来简单看一下 Promise实现的链式调用代码,如下所示。

  const read = () => {
    return new Promise((resolve, reject) => {
      fs.readFile(url, "utf-8", (err, data) => {
        if (err) {
          reject(err);
        }
        resolve(data);
      });
    });
  };
  read(A)
    .then((data) => read(B))
    .then((data) => read(C))
    .then((data) => read(d))
    .catch((reason) => console.log(reason));

  结合上面的代码,我们一起来分析一下 Promise内部的状态流转情况,Promise对象在被创建出来时是待定的状态,它让你能够把异步操作返回最终的成功值或者失败原因,和相应的处理程序关联起来。

  一般 Promise在执行过程中,必然会处于以下几种状态之一。

  1. 待定(pending):初始状态,既没有被完成,也没有被拒绝
  2. 已完成(fulfilled):操作成功完成。
  3. 已拒绝(rejected):操作失败。

  待定状态的 Promise对象执行的话,最后要么会通过一个值完成,要么会通过一个原因被拒绝。当其中一种情况发生时,我们用 Promisethen方法排列起来的相关处理程序就会被调用。因为最后 Promise.prototype.thenPromise.prototype.catch 方法返回的是一个 Promise, 所以它们可以继续被链式调用。

  关于 Promise 的状态流转情况,有一点值得注意的是,内部状态改变之后不可逆,你需要在编程过程中加以注意。文字描述比较晦涩,我们直接通过一张图就能很清晰地看出 Promise 内部状态流转的情况,如下所示(图片来源于网络)。
Promise 内部状态流转的情况

  从上图可以看出,我们最开始创建一个新的 Promise返回给 p1,然后开始执行,状态是 pending,当执行 resolve之后状态就切换为 fulfilled,执行 reject之后就变为 rejected的状态。

Promise 如何解决回调地狱

  首先,再回想一下什么是回调地狱,回调地狱有两个主要的问题:

  1. 多层嵌套的问题
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

  这两种问题在“回调函数时代”尤为突出,Promise的诞生就是为了解决这两个问题。Promise利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡。

下面通过一段代码来说明,如下所示。

  const readFilePromise = (filename) => {
    return new Promise((resolve, reject) => {
      fs.readFile(fliename, (err, data) => {
        if (err) {
          reject(err);
        } else {
          resolve(data);
        }
      });
    });
  };
  readFilePromise("1.json").then((data) => readFilePromise("2.json"));

  从上面的代码中可以看到,回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定。接下来我们针对上面的代码做一下微调,如下所示。

  let x = readFilePromise('1.json').then(data=>{
    return readFilePromise('2.json') //这里返回promsie
  });
  x.then(
    /* 内部逻辑省略 */
  )

  我们根据 then中回调函数的传入值创建不同类型的 Promise,然后把返回的 Promise穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。这便是返回值穿透的效果,这两种技术一起作用便可以将深层的嵌套回调写成下面的形式。

  readFilePromise("1.json")
    .then((data) => {
      return readFilePromise("2.json");
    })
    .then((data) => {
      return readFilePromise("3.json");
    })
    .then((data) => {
      return readFilePromise("4.json");
    });

  这样就显得清爽了许多,更重要的是,它更符合人的线性思维模式,开发体验也更好,两种技术结合产生了链式调用的效果。

  这样解决了多层嵌套的问题,那另外一个问题,即每次任务执行结束后分别处理成功和失败的情况怎么解决的呢?Promise采用了错误冒泡的方式。其实很容易理解,我们来看看效果。

  readFilePromise("1.json")
    .then((data) => {
      return readFilePromise("2.json");
    })
    .then((data) => {
      return readFilePromise("3.json");
    })
    .then((data) => {
      return readFilePromise("4.json");
    })
    .catch((err) => {
      // xxx
    });

  这样前面产生的错误会一直向后传递,被 catch接收到,就不用频繁地检查错误了。从上面的这些代码中可以看到,Promise解决效果也比较明显:实现链式调用,解决多层嵌套问题;实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题。

Promise 的静态方法

  从语法、参数以及方法的代码几个方面来分别介绍 allallSettledanyrace这四种方法。

all 方法

语法Promise.all(iterable)

参数: 一个可迭代对象,如 Array

描述: 此方法对于汇总多个 promise的结果很有用,在 ES6 中可以将多个 Promise.all异步请求并行操作,返回结果一般有下面两种情况。

  1. 当所有结果成功返回时按照请求顺序返回成功。
  2. 当其中有一个失败方法时,则进入失败方法。

  我们来看下业务的场景,对于下面这个业务场景页面的加载,将多个请求合并到一起,用 all来实现可能效果会更好,请看代码片段。

//1.获取轮播数据列表

  function getBannerList() {
    return new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve("轮播数据");
      }, 300);
    });
  }

  //2.获取店铺列表

  function getStoreList() {
    return new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve("店铺数据");
      }, 500);
    });
  }

  //3.获取分类列表

  function getCategoryList() {
    return new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve("分类数据");
      }, 700);
    });
  }

  function initLoad() {
    Promise.all([getBannerList(), getStoreList(), getCategoryList()])

      .then((res) => {
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      });
  }

  initLoad();

  从上面代码中可以看出,在一个页面中需要加载获取轮播列表、获取店铺列表、获取分类列表这三个操作,页面需要同时发出请求进行页面渲染,这样用 Promise.all来实现,看起来更清晰、一目了然

allSettled 方法

  Promise.allSettled 的语法及参数跟 Promise.all类似,其参数接受一个 Promise的数组,返回一个新的 Promise。唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled全部处理完成后,我们可以拿到每个 Promise的状态,而不管其是否处理成功。

const resolved = Promise.resolve(2);

  const rejected = Promise.reject(-1);

  const allSettledPromise = Promise.allSettled([resolved, rejected]);

  allSettledPromise.then(function (results) {
    console.log(results);
  });

  // 返回结果:

  // [

  //    { status: 'fulfilled', value: 2 },

  //    { status: 'rejected', reason: -1 }

  // ]

  从上面代码中可以看到,Promise.allSettled 最后返回的是一个数组,记录传进来的参数中每个 Promise的返回值,这就是和 all方法不太一样的地方。你也可以根据 all方法提供的业务场景的代码进行改造,其实也能知道多个请求发出去之后,Promise最后返回的是每个参数的最终状态。

any 方法

语法: Promise.any(iterable)

参数: iterable可迭代的对象,例如 Array

描述: any方法返回一个 Promise,只要参数 Promise实例有一个变成 fulfilled状态,最后 any返回的实例就会变成 fulfilled状态;如果所有参数 Promise实例都变成 rejected状态,包装实例就会变成 rejected状态。

  还是对上面 allSettled这段代码进行改造,我们来看下改造完的代码和执行结果。

  const resolved = Promise.resolve(2);

  const rejected = Promise.reject(-1);

  const anyPromise = Promise.any([resolved, rejected]);

  anyPromise.then(function (results) {
    console.log(results);
  });

  // 返回结果:

  // 2

  从改造后的代码中可以看出,只要其中一个 Promise变成 fulfilled状态,那么 any最后就返回这个 Promise。由于上面 resolved这个 Promise已经是 resolve的了,故最后返回结果为 2。

race 方法

语法: Promise.race(iterable)

参数: iterable可迭代的对象,例如 Array

描述: race方法返回一个 Promise,只要参数的 Promise之中有一个实例率先改变状态,则 race方法的返回状态就跟着改变。那个率先改变的 Promise实例的返回值,就传递给 race方法的回调函数。

  我们来看一下这个业务场景,对于图片的加载,特别适合用 race方法来解决,将图片请求和超时判断放到一起,用 race来实现图片的超时判断。请看代码片段。

//请求某个图片资源

  const requestImg = () => {
    var p = new Promise(function (resolve, reject) {
      var img = new Image();

      img.onload = function () {
        resolve(img);
      };
      img.src = "http://www.baidu.com/img/flexible/logo/pc/result.png";
    });
    return p;
  };

  //延时函数,用于给请求计时

  const timeout = () => {
    var p = new Promise(function (resolve, reject) {
      setTimeout(function () {
        reject("图片请求超时");
      }, 5000);
    });
    return p;
  };

  Promise.race([requestImg(), timeout()])
    .then(function (results) {
      console.log(results);
    })
    .catch(function (reason) {
      console.log(reason);
    });

  从上面的代码中可以看出,采用 Promise的方式来判断图片是否加载成功,也是针对 Promise.race 方法的一个比较好的业务场景。

总结

  整理了一下 Promise 的几个方法:

Promise方法简单总结
all参数所有返回结果为成功才返回
allSettled参数不论返回结果是否成功,都返回每个参数执行状态
any参数中只要有一个成功,就返回该成功的执行结果
race顾名思义,返回最先返回执行成功的参数的执行结果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值