处理异步操作对象 —— Promise

103 阅读12分钟

如何获取异步操作的结果?

传统方案是使用回调函数,在异步操作结束之后,执行回调函数,并将结果作为参数传递出去。

回调函数

但是回调函数会有两个缺点:

1. 回调地狱

例如:异步方法要依赖前一次结束,所以只能在对方的回调函数里面执行。

fn1(function(value1){
    fn2(value1, function(value2){
        fn3(value2, function(value3){
            fn4(value3, function)(value4){
              //...
            }
        })
    })
})

这样就造成了一层层回调函数嵌套,也就是回调地狱,让函数可读性和可维护性变得非常差。

2. 并行问题

例如:需要等待三个接口都请求成功之后,再去执行某个操作,怎么知道三个接口什么时候全部请求成功呢?

(1)对三个接口分别设置三个不同的变量,执行成功后修改变量的值,在每个接口判断其他两个变量值

let isAjaxASuccess = false, 
    isAjaxBSuccess = false, 
    isAjaxCSuccess = false;
function ajaxA (callback) {
    // 请求成功后更新状态
    isAjaxASuccess = true;
    if (isAjaxBSuccess && isAjaxCSuccess) {
        callback();
    }
}
function ajaxB (callback) {
    // 请求成功后更新状态
    isAjaxBSuccess = true;
    if (isAjaxCSuccess && isAjaxCSuccess) {
        callback();
    }
}
function ajaxC (callback) {
    // 请求成功后更新状态
    isAjaxCSuccess = true;
    if (isAjaxCSuccess && isAjaxBSuccess) {
        callback();
    }
}

(2)设置 setInterval 进行轮询,直到三个接口都请求成功

let isASuccess = false, 
    isBSuccess = false, 
    isCSuccess = false;
function ajaxA () {
    // 请求成功后更新状态
    isASuccess = true;
}
function ajaxB () {
    // 请求成功后更新状态
    isBSuccess = true;
}
function ajaxC () {
    // 请求成功后更新状态
    isCSuccess = true;
}
const interval = setInterval(() => {
    if (isASuccess && isBSuccess && isCSuccess) {
        callback();
        clearInterval(interval);
    }
}, 500)

以上两种方法都需要维护每个接口的请求状态,如果后续还需要加接口,代码扩展性很差。

基于以上两个问题,ES6 引入了一种用于处理异步操作的对象 —— Promise

Promise

Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

Promise 对象有以下两个特点:

  1. 对象的状态不受外界影响。

Promise 有三种状态:

  • 进行中:pending
  • 已成功:fulfilled
  • 已失败:rejected
let promise = new Promise(function(resolve,reject){
  // 异步操作
  // 当异步操作成功的时候执行resolve(),就可以将承诺的状态由pending -> fulfilled
  // 当异步操作失败的时候执行reject(),就可以将承诺的状态由pending -> rejected
})

只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。

上面三种状态里面,fulfilled 和 rejected 合在一起称为 resolved(已定型)。代表状态已经凝固,不会再变了,会一直保持这个结果。

为了便于理解,我们通常把 resolved 只指 fulfilled 状态,不包含 rejected 状态。

Promise 构造函数

JavaScript 提供原生的 Promise 构造函数,用来生成 Promise 实例。

const promise = new Promise((resolve, reject) => {
  // ...
  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

上面代码中,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject 。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。

  • resolve 函数的作用是,将 Promise 实例的状态从 “ 未完成 ” 变为 “ 成功 ”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。

  • reject 函数的作用是,将 Promise 实例的状态从 “ 未完成 ” 变为 “ 失败 ”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise.prototype.then()

Promise.prototype.then(resolved_callback, rejected_callback)

  • resolved_callback:当承诺对象状态 pending -> resolved 时的回调函数

  • rejected_callback: 当承诺对象状态 pending -> rejected 时的回调函数(该参数可以省略)

每一个 .then() 方法还会返回一个新生成的 promise 对象,这个对象可被用作链式调用,就像这样:

const promise = new Promise((resolve, reject) => {
  // ...
  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

promise
  .then(handleResolvedA)
  .then(handleResolvedB)
  .then(handleResolvedC, handleRejectedC);

上面代码中,promise 后面有三个 then ,意味依次有三个回调函数,只要前一步的状态变为 fulfilled,就会依次执行紧跟在后面的回调函数。

最后一个 then 方法,handleResolvedC 只能获取 handleResolvedB 的返回值,而 handleRejectedC 可以获取 handleResolvedAhandleResolvedB 任意一个发生的错误。假如 handleResolvedA 的状态变为 rejected,那么 handleResolvedB 不会执行,因为是 resolved 的回调函数。Promise 开始寻找,直到找到第一个为 rejected 的回调函数 handleRejectedC。这就是说,Promise 对象的报错具有传递性。

下面是 Promise 的四种不同写法:

function f1() {
  return new Promise(function(resolve, reject) {
    resolve('f1 运行结果');
    return;
  })
}
function f2(params) {
  console.log('f2 的参数是:', params);
  return 'f2 运行结果';
}
function f3(params) {
  console.log('f3 的参数是:', params);
}

// 写法一
f1().then(function () {
  return f2();
}).then(f3);
// f3 的参数是 f2 运行的结果

// 写法二
f1().then(function () {
  f2();
  return;
}).then(f3);
// f3 的参数是 undefined

// 写法三
f1().then(f2()).then(f3);
// f3 的参数是 f2 返回的函数的运行结果

// 写法四
f1().then(f2).then(f3);
// f3 的参数是 f2 运行的结果,f2 的参数是 f1 运行的结果

上面代码中,

写法一:f1.then 方法的回调函数返回了 f2 的运行结果,这个结果作为下一个 .then 方法的回调函数的参数,也就是 f3 的参数;

image.png

写法二:f1.then 方法的回调函数运行完 f2 之后,直接 return ,所以返回值是 undefined,下一个 .then 方法的回调函数 f3 的参数也就是 undefined;

image.png

写法三:f1.then 方法会新生成 Promise 对象,即 f2 运行之后返回的函数,这个 Promise 运行的的结果就是是 f3 的参数;

image.png

写法四:f1 的运行之后返回的结果作为它的 .then 方法的回调函数的参数,所以 f2 的参数是 f1 运行的结果,f3 的参数是 f2 运行的结果

image.png

Promise.prototype.catch()

Promise.prototype.catch(rejected_callback)

  • rejected_callback:当承诺对象的状态pending -> rejected 时的回调函数

不管是ajax交互中出现的404、500异常会被 catch 捕获,在 then 中的代码语法错误也会被 catch 捕获。

Promise.prototype.catch() 方法是 .then(null, rejection).then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。

promise.then(function(res) {
  // ...
}).catch(function(error) {
  // 处理 promise 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,promise 方法返回一个 Promise 对象,

  • 如果该对象状态变为 resolved,则会调用 then() 方法指定的回调函数;
  • 如果异步操作抛出错误,状态就会变为 rejected,就会调用 catch() 方法指定的回调函数,处理这个错误。

另外,then() 方法指定的回调函数,如果运行中抛出错误,也会被 catch() 方法捕获。

一般来说,不要在 then() 方法里面定义 reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。

// bad
promise.then(function(res) {
  // success
}, function(err) {
  // error
})

// good
promise.then(function(data) {
  // success
})
.catch(function(err) {
  // error
});

上面代码中,第二种写法可以捕获前面 then 方法执行中的错误,也更接近同步的写法。因此,建议总是使用 catch() 方法,而不使用 then() 方法的第二个参数。

Promise.prototype.finally()

Promise.prototype.finally(callback)

不管承诺的状态最终如何,该函数中的回调函数都会执行

promise
  .then(result => {···})
  .catch(error => {···})
  .finally(() => {···});

上面代码中,不管 promise 最后的状态,在执行完 thencatch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。

finally 方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是 fulfilled 还是 rejected。这表明,finally 方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally 本质上是 then 方法的特例:

promise
.finally(() => {
  // ...
});

// 等同于

promise
.then(
  result => {
    // ...
    return result;
  },
  error => {
    // ...
    throw error;
  }
);

上面代码中,如果不使用 finally 方法,同样的语句需要为成功和失败两种情况各写一次。有了 finally 方法,则只需要写一次。

Promise.all()

Promise.all([promise1, promise2, promise3,...])

Promise.all() 方法接受一个数组(可以不是数组,但必须具有 Iterator 接口)作为参数,promise1、promise2、promise3 都是 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中,p 的状态由 p1p2p3 决定,分成两种情况:

(1)当 p1p2p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1p2p3 的返回值组成一个数组,传递给 p 的回调函数。

(2)只要 p1p2p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

// 生成一个Promise对象的数组
const promiseList = arr.map(function (item) {
  return getJSON('/post/' + item.id + ".json");
});
Promise.all(promiseList)
  .then(function (res) {
    // ...
  }).catch(function(err){
    // ...
  });

上面代码中,promiseList 是 Promise 实例的数组。

  • 只有实例的状态都变成 fulfilled,才会调用 Promise.all 方法后面的 .then 方法的回调函数

  • 或者其中有一个变为 rejected,才会调用 Promise.all 方法后面 .catch 方法的回调函数

注意,如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 reject,并不会触发 Promise.all()catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
  .then(result => result)
  .catch(e => e);

Promise.all([p1, p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));
// ["hello", Error: 报错了]

上面代码中,p1 会 resolved,p2 首先会 rejected,但是 p2 有自己的 catch 方法,该方法返回的是一个新的 Promise 实例,p2 指向的实际上是这个实例。该实例执行完 catch 方法后,也会变成 resolved,导致 Promise.all() 方法参数里面的两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数。

image.png

如果 p2 没有自己的 catch 方法,就会调用 Promise.all()catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
  .then(result => result)
  .catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
  .then(result => result)

Promise.all([p1, p2])
  .then(result => console.log(result))
  .catch(e => console.log(e));

image.png

Promise.race()

Promise.race([promise1, promise2, promise3,...])

Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要 p1p2p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

例:如果指定时间内没有获得结果,就将 Promise 的状态变为 reject,否则变为 resolve。

const p = Promise.race([
  getJSON('/post'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);
p.then(console.log)
 .catch(console.error);

上面代码中,如果 5 秒之内 getJSON 方法无法返回结果,p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数。

Promise.allSettled()

Promise.allSettled([promise1, promise2, promise3,...])

Promise.allSettled() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。

该方法返回的新的 Promise 实例,一旦结束,状态总是 fulfilled,不会变成 rejected。状态变成fulfilled 后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入 Promise.allSettled() 的 Promise 实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

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

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ]

上面代码中,Promise.allSettled() 的返回值 allSettledPromise,状态只可能变成 fulfilled。

image.png

它的监听函数接收到的参数是数组 results。该数组的每个成员都是一个对象,对应传入 Promise.allSettled() 的两个 Promise 实例。每个对象都有 status 属性,该属性的值只可能是字符串 fulfilled 或字符串 rejected。fulfilled 时,对象有 value 属性,rejected 时有 reason 属性,对应两种状态的返回值。

微任务

Promise 的回调函数属于异步任务,会在同步任务之后执行。

new Promise(function (resolve, reject) {
  resolve(1);
}).then(console.log);

console.log(2);
// 2
// 1

上面代码会先输出2,再输出1。因为 console.log(2) 是同步任务,而 then 的回调函数属于异步任务,一定晚于同步任务执行。

image.png

异步任务分为:

  • 宏任务:setTimeout, setInterval,...

  • 微任务:Promises,...

宏任务、微任务的执行顺序

  1. 先执行同步代码,
  2. 遇到异步宏任务则将异步宏任务放入宏任务队列中,
  3. 遇到异步微任务则将异步微任务放入微任务队列中,
  4. 当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,
  5. 微任务执行完毕后再将异步宏任务从队列中调入主线程执行,
  6. 一直循环直至所有任务执行完毕。
setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function (resolve, reject) {
  console.log(2)
  resolve(3);
}).then(console.log);

console.log(4);
// 2
// 4
// 3
// 1

上面代码,

  1. 遇到 setTimeout,异步宏任务,放入宏任务队列中;
  2. 遇到 new Promise,Promise 在实例化的过程中所执行的代码都是同步进行的,所以输出 2;
  3. Promise.then 中指定的回调是异步执行的,将其放入微任务队列中;
  4. 遇到同步任务 console.log('4'); 输出 4;主线程中同步任务执行完;
  5. 从微任务队列中取出任务到主线程中,输出 3,微任务队列为空;
  6. 从宏任务队列中取出任务到主线程中,输出 1,宏任务队列为空,结束

image.png