30分钟带你玩转Promise

156 阅读8分钟

话不多说,进入正题

Promise的特点

Promise包括pending(进行中)、fulfilled(已成功)和rejected(已失败)这三种状态, 初始状态为pending, 一旦变为fullfilled或者rejected, 这种改变不可逆转, 一切取决于操作的结果. 每一次链式回调返回的都是一个Promise实例.

与事件的区别

Event: 如果你错过了Event, 再去监听, 是得不到结果的. Promise: 如果改变已经发生了, 你再对Promise对象添加回调函数, 也会立即得到这个结果.

Promise的缺点

从时间处理的先后顺序上来看, 有以下缺点

  1. 无法中途取消
  2. Promise会"吃掉错误", 如果不设置catch方法进行处理, 会中止代码执行
  3. catch仅能处理Promise链式调用上面在它前面的错误, 后面的错误无法捕获
  4. then方法有两个回调函数分别为successCallback和errorCallback, 如果第二个函数抛出了错误, 即使在后面的调用链上使用catch, 也捕捉不到错误
  5. pending状态不确定性: 可能在刚刚开始, 也可能即将完成

面试官问:请实现一个sleep函数

function sleep(times) {
return new Promise((resolve, reject) => {
  // 从第三个参数开始, 依次为传递给第一个参数resolve函数的形参
  setTimeout(resolve, times, 'done')    
}).then(
  (res) => {// 睡醒后干的事}
)
}
sleep(1000).then((value) => {
  console.log(value);
})

Promise 新建后就会立即执行

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

解释:在代码执行阶段, Promise立即执行, 所以首先输出Promise, 由于then为微任务, 先塞到微任务队列中,等待所有同步任务执行完成之后, 在事件循环的末尾, 再一次性排空执行完成所有微任务队列中的任务, 所以再次输出Hi, 最后输出resolved

注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

先输出2, 再输出1, 道理同上

最佳实践

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

注意:then()方法指定的回调函数,如果运行中抛出错误,也会被后续的调用链式上的catch()方法捕获。

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

注意:reject(new Error(reason))等同于throw new Error(reason).很好理解,就不举例了

注意:Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。一旦捕获,中止冒泡。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应,也就是说会报错误,但不会中止代码执行

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
}).catch(e => console.log(e));

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

解释:上面代码中,someAsyncThing()函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

正解

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
}).catch(e => console.log(e));

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

Promise.prototype.finally

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

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

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

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

它的实现也很简单。

Promise.prototype.finally = callback => {
let then = this.then
// 等同于 let then = Promise.prototype.then
return then(value => Promise.resolve(callback()).then(() => value), reason => Promise.resolve(callback()).then(() => {throw reason}))
}

解释:最后面加上的then回调函数是为了保证Promise后续的链式回调能正常使用

上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。

从上面的实现还可以看到,finally方法总是会返回原来的值。

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})

Promise.protoype.all

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发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方法指定的回调函数。 如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。

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

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));
// Error: 报错了

Promise.race

顾名思义, race: 赛跑

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

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

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

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

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

利用这个特性可以做接口超时应急处理

面试官问:设计实现一个Promise.race?

Promise.race = promises => new Promise((resolve, reject) => {
        promises.forEach(promise => {                      
             promise.then(resolve).catch(reject)
   })
})

Promise.allSettled

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

const promises = [
  fetch('/api-1'),
  fetch('/api-2'),
  fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失.

应用:因此可以对特定的N个请求做loading关联.

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

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

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

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

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

下面是返回值用法的例子。

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);

// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');

// 过滤出失败的请求,并输出原因
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。 这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。这个时候使用Promise.all就不合适了,因为你不能确保所有的请求都已经执行完成,有可能只有一个请求失败为rejected。

有这样一种场景:后台管理系统点击一个按钮之后,需要调用N个请求,同时马上禁用这个按钮;当N个请求都完成之后,不管成功失败与否,都要放开这个按钮的使用。

麻烦的写法:

const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x));

try {
  await Promise.all(requests);
  console.log('所有请求都成功。');
} catch {
  console.log('至少一个请求失败,其他请求可能还没结束。');
}

解释:上述代码如果所有的请求都执行成功就会打印"所有请求都成功。", 只要有一个请求失败抛出错误,就会被try捕捉到进而中止try代码块中下面的代码执行,而执行catch语句,这个时候回打印"至少一个请求失败,其他请求可能还没结束。" 同样,Promise.all()无法确定所有请求都结束,有了Promise.allSettled(),这就很容易了。

面试官问:设计实现一个Promise.allSettled?

Promise.allSettled = async promises => new Promise((resolve, reject) => {
   let _promises = [];
   await promise.forEach(promise =>      
   promise.then(value => {_promises.push({status: 'fulfilled', value})}).catch(reason => {_promises.push({status:     
   'rejected', reason})})) 
    resolve(_promises)
})  

Promise.any对照Promise.all,它两刚好相反,略过

不积跬步,无以至千里,不积小流,无以成江海.

每天进步一点,看见不一样的自己!