一文搞懂js Promise

很多小伙伴学JavaScript学到异步的部分就卡壳了。

异步编程允许我们在执行一个长时间任务时,程序不需要进行等待,而是继续执行之后的代码,直到这些任务完成之后再回来通知你,通常是以回调函数(callback)的形式。

getUserPost(function (userPost) {
  console.log(userPost);
});

「异步」这种编程模式避免了程序的阻塞,大大提高了 CPU 的执行效率,尤其适用于 IO 密集的——例如需要经常执行网络操作、数据库访问的应用。

这篇文章,我主要是以 JavaScript 为例向大家介绍异步编程、Promise。

在 JavaScript 中有两种实现异步的方式。

第一种是传统的「回调函数」,比如我们可以使用setTimeout让一个函数在指定的时间后执行。

setTimeout(() => {
  console.log("123");
}, 3000);

console.log("456");

setTimeout这个函数本身会立刻返回,程序会紧接着执行之后的代码console.log("456"),而我们传入的回调函数() => {console.log("123");}等到预定的时间才会执行。

这里需要注意的是,JavaScript从设计之初就是一个「单线程」的编程语言。

即便上面那段代码乍一看上去,里面的回调函数和主程序在并发执行,但它们都运行在同一个主线程中。

实际上主线程中还运行了我们写的其他代码,包括界面逻辑、网络请求、数据处理等等。

主线程

虽然只有单个线程在执行,但这种单线程的异步编程方式有很多优点。

由于所有操作都运行在同一个线程中,所以我们无需考虑线程同步或资源竞争的问题,并且从源头上避免了线程之间的频繁切换,从而降低了线程自身的开销。

回调函数虽然简单、容易理解:

setTimeout(() => {
  console.log("等三秒后");
  
  // ...
}, 3000);

但它有一个明显的缺点,如果我们需要执行多个异步操作,我们的代码可能会写成这样:

setTimeout(() => {
  console.log("等三秒后");
  setTimeout(() => {
    console.log("再等三秒");
    setTimeout(() => {
      console.log("又等三秒");
      // ...
    }, 3000);
  }, 3000);
}, 3000);

当第一个任务执行完毕后,再在回调函数里执行第二个任务,然后是第三个、第四个……

整个程序会一层接着一层地嵌套下去,可读性会变得非常之差。

这种情况也被称作「回调地狱」(Callback Hell)。

其实,Promise之所以产生,就是为了解决「回调地狱」的问题的。

JavaScript中使用 Promise 的 API 「fetch」就是一个很好的例子。

fetch('http://...')

fetch()函数返回的是一个 Promise 对象。

这里的Promise几乎就是它的字面意思——承诺。

承诺什么呢?承诺这个请求会在未来某个时刻返回数据。

我们随后可以调用它的then方法并传递一个回调函数。如果这个请求在未来成功完成,那么回调函数会被调起,请求的结果也是以参数的形式传递进来:

fetch("链接")
  .then((response) => {
    // ...
  });

当然,如果光是这样,Promise和回调函数也就没有什么区别了。

其实Promise的优点主要在于它可以用一种链式结构将多个异步操作串联起来。

fetch("链接")
  .then((response) => response.json());

比如这里的response.json()方法也会返回一个Promise对象,它代表在未来的某个时刻,将返回的数据转换为JSON格式。如果我们想要等到它完成之后再执行其它操作,我们可以在后面追加一个then

fetch("链接")
  .then((response) => response.json())
  .then((json) => console.log(json));

Promise的链式调用避免了「回调地狱」的问题,可读性大大提升。

在使用异步操作的时候,我们也可能会遇到错误,比如各种网络问题或返回的数据格式不正确等等。

怎么捕获这些错误?

最简单的方法是附加一个catch在链式结构的末尾:

fetch("链接")
  .then((response) => response.json())
  .then((json) => {
    console.log(json);
  })
  .catch((error) => {
    console.error(error);
  });

如果之前任意一个阶段发生了错误,那么catch将会被触发,而之后的then将不会被执行,这和同步编程中用到的try/catch块很类似。

Promise还提供finally方法,它会在Promise链结束之后调用,无论失败与否,我们可以在这里做一些清理工作。

fetch("链接")
  .then((response) => response.json())
  .then((json) => {
    console.log(json);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    // 执行清理操作等等
  });

比如,如果我们用到了加载动画,就可以在finally中关闭它。