一、什么是异步

异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。总所周知,JavaScript 的代码执行的时候是跑在单线程上的,代码按照出现的顺序,从上到下一行一行的执行,也就是我们说的同步(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。

简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

实际开发中,某些业务的结果我们是不能同步获取的,而等待的结果也是不确定的。比如异步操作 ajax 获取数据,遍历一个大型的数组(同步操作),还有动态加载脚本文件然后初始化相关业务。

function loadScript(src) {
  let script = document.createElement("script");
  script.src = src;
  document.head.append(script);
}
loadScript("./js/script.js");
init(); // 定义在 ./js/script.js 里的函数

这样的代码肯定是达不到效果的。因为加载脚本是需要花时间的,是一个异步的行为,浏览器执行 JavaScript 的时候并不会等到脚本加载完成的时候再去调用 init 函数。

远古时代的做法是使用回调函数,给处理函数传递一个回调函数来处理回调结果。

function loadScript(src, success, fail) {
  let script = document.createElement("script");
  script.src = src;
  script.onload = success;
  script.onerror = fail;
  document.head.append(script);
}
loadScript("./js/script.js", success, fail);
function success() {
  console.log("success");
  init(); // 定义在 ./js/script.js 中的函数
}
function fail() {
  console.log("fail");
}

试想一下,如果 success 函数又需要异步处理回调呢?OMG,那简直就是灾难,传说中的"回调地狱"青面獠牙向我们走来,它是恶魔,即使是聪明绝顶的程序猿都避而远之。

为了避免"回调地狱"吞噬程序猿茂密的毛发,Promise解决方案就应运而生了。

二、Promise

Promise,是用来处理异步操作的,可以让我们写异步调用的时候写起来更加优雅,更加美观,人称“护发金刚”。顾名思义为承诺、许诺的意思,意思是使用了 Promise 之后他肯定会给我们答复,无论成功或者失败都会给我们一个答复。

我们可以把异步操作交给 Promise 来处理,什么时候处理好他通知我们,如果还有异步操作再交给 Promise 处理,这样就可以将异步的操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

俗话说,神仙也有缺点。当然 Promise 也不例外。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

纵使她有这些缺点,但丝毫影响我们对他的欢喜,甚至有些猿友对她爱得死去活来的。

Promise 首先是一个对象,它通常用于描述现在开始执行,一段时间后才能获得结果的行为(异步行为),内部保存了该异步行为的结果。然后,它还是一个有状态的对象:

  • pending:待定
  • fulfilled:兑现,有时候也叫解决(resolved)
  • rejected:拒绝

一个 Promise 只有这 3 种状态,且状态的转换过程有且仅有 2 种:

  • pending 到 fulfilled
  • pending 到 rejected

三个属性,两个技能,她就是"回调地狱"的克星,程序猿的知音啊。

创建 Promise

调用 Promise 构造函数来创建一个 Promise。

let promise = new Promise((resolve, reject) => {});

Promise 构造函数接收一个函数作为参数,该函数的两个参数是 resolve,reject,它们由 JavaScript 引擎提供。

其中 resolve 函数的作用是当 Promise 对象转移到成功,调用 resolve 并将操作结果作为其参数传递出去;reject 函数的作用是当 Promise 对象的状态变为失败时,将操作报出的错误作为其参数传递出去。

由 new Promise 构造器返回的 Promise 对象具有如下内部属性:

  • PromiseState:最初是 pending,resolve 被调用的时候变为 fulfilled,或者 reject 被调用时会变为 rejected。
  • PromiseResult:最初是 undefined,resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error。
三、Promise 常用方法

Promise.prototype.then()

调用 then 可以为实例注册两种状态的回调函数,当实例的状态为 fulfilled,会触发第一个函数执行,当实例的状态为 rejected,则触发第二个函数执行。

  • onResolved:状态由 pending 转换成 fulfilled 时执行。
  • onRejected:状态由 pending 转换成 rejected 时执行。
function onResolved(res) {
  console.log("resolved" + res); // resolved3
}
function onRejected(err) {
  console.log("rejected" + err);
}
new Promise((resolve, reject) => {
  resolve(5);
}).then(onResolved, onRejected);

Promise.prototype.catch()

catch 只接受一个参数,也就是 rejected 抛出的值,一般用于异常处理。传统的try/catch捕获不了Promise内部的异常的,因为抛出异常这个动作是异步的。

在处理异常的时候,我们可以在catch中进行异常的捕获,也可以直接抛出异常。

function onRejected(err) {}
new Promise((resolve, reject) => {
  reject();
}).catch(onRejected);

Promise.prototype.finally()

finally()方法只有当状态变化的时候才会执行,可以用来做一些程序的收尾工作,比如操作文件的时候关闭文件流。

function onFinally() {
  console.log(12345); // 并不会执行
}
new Promise((resolve, reject) => {}).finally(onFinally);

all()

Promise 的 all 方法提供了并行执行异步操作的能力,在 all 中所有异步操作结束后才执行回调。

function p1() {
  var promise1 = new Promise(function (resolve, reject) {
    console.log("p1的第一条输出语句");
    resolve("p1完成");
  });
  return promise1;
}

function p2() {
  var promise2 = new Promise(function (resolve, reject) {
    console.log("p2的第一条输出语句");
    setTimeout(() => {
      console.log("p2的第二条输出语句");
      resolve("p2完成");
    }, 2000);
  });
  return promise2;
}

function p3() {
  var promise3 = new Promise(function (resolve, reject) {
    console.log("p3的第一条输出语句");
    resolve("p3完成");
  });
  return promise3;
}

Promise.all([p1(), p2(), p3()]).then(function (data) {
  console.log(data);
});

输出结果:

p1的第一条输出语句;
p2的第一条输出语句;
p3的第一条输出语句;
p2的第二条输出语句[("p1完成", "p2完成", "p3完成")];

race()

在all中的回调函数中,等到所有的Promise都执行完,再来执行回调函数,race则不同它等到第一个Promise改变状态就开始执行回调函数。将上面的all改为race

function p1(){
  var promise1 = new Promise(function(resolve,reject){
      console.log("p1的第一条输出语句");
      resolve("p1完成");
  })
  return promise1;
}

function p2(){
  var promise2 = new Promise(function(resolve,reject){
      console.log("p2的第一条输出语句");
      setTimeout(() => {
        resolve("p2完成");
      }, 2000);
  })
  return promise2;
}

function p3(){
  var promise3 = new Promise(function(resolve,reject){
      console.log("p3的第一条输出语句");
      resolve("p3完成")
  });
  return  promise3;
}

Promise.race([p1(),p2(),p3()]).then(function(data){
  console.log(data);  
})

结果:

p1的第一条输出语句
p2的第一条输出语句
p3的第一条输出语句
p1完成

p1完成返回后就不等p2,p3的返回了。

四、练习题

1、下面的代码输出什么?

new Promise((resolve, reject) => {
  console.log('A')
  resolve(3)
  console.log('B')
}).then(res => {
  console.log('C')
})
console.log('D')
// 打印结果:A B D C

上面这串代码的输出顺序是:A B D C。

解答

我们知道,立即执行函数会在 new Promise 调用的时候同步执行。所以为先打印出AB,然后遇到resolve(3)onResolved函数会被推入微任务队列,然后就打印D,此时所有同步任务执行完成,浏览器会去检查微任务队列,发现存在一个,所以最后会去调用 onResolved 函数,打印出 C。

注意,执行 resolve()/reject()/finally()时都是异步的。

2、下面的代码输出什么?

new Promise((resolve, reject) => {
    resolve(1)
}).then(res => {
    console.log('A')
}).finally(() => {
    console.log('B')
})
new Promise((resolve, reject) => {
    resolve(2)
}).then(res => {
    console.log('C')
}).finally(() => {
    console.log('D')
})
// 打印结果:A C B D

上面这串代码的输出顺序是:A C B D。

解答

  • 执行 resolve(1),将处理程序 A 推入微任务队列 1;
  • 执行 resolve(2),将处理程序 C 推入微任务队列 2;
    同步任务执行完成,执行微任务队列 1 里的内容,打印 A,A 所在函数执行完成后生成了一个 fulfilled 的新实例,由于新实例状态变化,所以会立即执行 finally() 处理程序 B 推入微任务队列 3;
  • 执行微任务队列 2 的内容,打印 C,C 所在函数执行完成后,同上条原理会将处理程序 D 推入微任务队列 4;
  • 执行微任务队列 3 的内容,打印 B;
  • 执行微任务队列 4 的内容,打印 D;
  • 代码全部执行完成,最终打印:A C B D。

最后

欢迎关注公众号【前端技术驿站】,回复5678获取最新前端实战视频