18 Promise及其简易实现

Posted by CodingWithAlice on April 13, 2021

18 Promise及其简易实现

参考文章:这一次,彻底弄懂 Promise 原理 - 对 Promise 源码进行了解析 - 没看完

30分钟,让你彻底明白Promise原理 - 真的看不进去,有需要再看吧

JS的异步编程模型,典型的如下图:

image-20210413205034686

异步回调

Web 页面的单线程架构决定了异步回调,而异步回调影响到了我们的编码方式。

–> 将异步回调函数封装是一个很好的思维,

–> 但是封装后,如果遇到比较复杂的业务场景 嵌套了太多的回调函数 就很容易使得自己陷入了回调地狱

代码之所以看上去很乱,归结其原因有两点:

  • 第一是 嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
  • 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的 错误处理 的方式,明显增加了代码的混乱程度。

Promise 解决了什么问题呢?

作用:管理异步方式返回的代码

解决的问题

  • 第一是消灭嵌套调用 -> 可以实现链式的写法来实现同步异步操作
  • 第二是合并多个任务的错误处理

Promise 优缺点

我们可以 注册任意数量的函数 在成功或者失败后运行,也可以 在任何时候注册 事件处理程序。

  • 优点

    回调函数写成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现很多强大的功能。如果一个任务已经完成,再添加回调函数,该回调函数会立即执行

  • 缺点

    1、无法取消Promise一旦新建/声明它就会立即执行,无法中途取消

    2、“Promise会吃掉错误” - 如果不设置 cathch/reject 回调函数,Promise 内部抛出的错误,不会反应到外部 -> 不影响 Promise 外部脚本的执行

    3、当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

Promise 怎么解决嵌套回调问题

1、Promise 实现了 回调函数的延时绑定 -> 创建完 Promise 后再用 .then 方法绑定回调函数

2、将回调函数 onResolve 的返回值穿透到最外层 -> 需要通过返回值确认创建什么类型的Promise任务

image-20210413205128118

Promise 怎么处理异常

即使一个Promise有多个回调函数,也可以通过一个 .catch 方法来捕获异常

–> Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject 函数处理或 catch 语句捕获为止

1/3、Promise 特点 -> 状态凝固

promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束。

resolve下面的语句其实是可以执行的,那么为什么 reject 的状态信息在下面没有接受到呢?

image-20210708164129103

new 出一个 Promise 对象 - 初始化实例时,这个对象的起始状态就是 Pending 状态,在根据 resolvereject 返回 Fulfilled 状态/ Rejected 状态 —> resolve 与 reject 只能执行一个

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

new Promise((resolve, reject) => {
    var param = '一朵🌹';
    if (error) {
        reject(new Error('Promise 异步执行异常'));
    } else {
        resolve(param);
    }
}).then((res) => {
    console.log(res);  //  一朵🌹
    var param = '🦕🦕🦕';
    return param;
}).then((res) => {
    console.log(res);  //  🦕🦕🦕
}).catch((error) => {
    console.log(error); //  Promise 异步执行异常
});

2/3、Promise 特点 - then

then 中返回了 新的 Promise 实例,但是 then 中注册的回调仍然是属于上一个 Promise 的;

then 函数是负责注册回调的,真正的执行是在 Promise 的状态被改变之后

image-20210708165548959

【补充】:

catch 方法是 then(null, rejection) 的别名 -> 也就是说 catch 方法也会 返回一个新的 Promise 实例

p.then((val) => console.log('fulfilled:', val))
    .catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
    .then(null, (err) => console.log("rejected:", err));

3/3、Promise 特点 - finally

不管 Promise 最后的状态如何,都要执行一些最后的操作

finally 回调函数不接受任何参数 - 我们把这些操作放到 finally 中,也就是说 finally 注册的函数是与 Promise 的状态无关的,不依赖 Promise 的执行结果。

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

Promise 为什么要使用微任务

我们先来简单实现一个Promise -> 仅为展示微任务相关,不作为 Promise 的简单代码实现

image-20210413205156971

因为定时器的执行效率不是太高,采用微任务实现了回调函数延迟绑定,这就是为什么Promise使用微任务的原由了

下文开始为 Promise 基础,参考博客 阮一峰-ES6

Promise 实现图片加载

var preloadImage =function(path){
    return new Promise(function(resolve, reject){
        var image =new Image(); // 实例
        image.onload  = resolve; // 监听属性1 - 在图片加载成功后调用
        image.onerror = reject; // 监听属性2 - 在加载失败调用
        image.src = path;
    });
};
preloadImage('https://example.com/my.jpg')
    .then(function(e){ document.body.append(e.target)})
    .then(function(){ console.log('加载成功')})

Promise 返回另一个 Promise 时,状态传递

const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('fail')), 3000);
})

const p2 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve(p1), 1000);
})

p2
    .then(result => console.log(result))
    .catch(error => console.log(error))
// Error: fail

上面代码详解:

p1 是一个 Promise,3秒后变为 reject;p2 的状态在 1秒后改变,resolve 返回 p1;

由于 p2 返回的是另一个 Promise,导致 p2 自己的状态无效 了, p2 的状态由 p1 决定;

p2 后面注册的 then 语句都变成针对 p1的 -> 再过 2秒, p1 变成 rejected ,导致触发 catch 指定的回调。

Promise.all

Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例

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

注意点:

  • 1、只有 p1、p2、p3 的状态 都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
  • 2、只要 p1、p2、p3 有一个被 rejected ,p 的状态就会变成 rejected ,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数
  • 3、如果作为参数的 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)) // 被执行了,输出了 p1 和 p2 的结果
    .catch(e => console.log(e));
// ["hello", Error: 报错了]

如上代码分析:p1 会 resolved, p2 首先会 rejected ,但是 p2 有自己的 catch 方法,该 catch 方法返回一个新的 Promise 实例,p2 实际上指向的是这个实例 -> 该实例 执行完 catch 之后,状态变成 resolved

—> 所以导致 Promise.all() 方法的两个 promise 实例都会 resolved ->因此会调用 then 方法指定的回调

考题:给出一个场景,要求A、B请求执行结束后,再执行C请求,其中A、B请求同时开始,怎么实现:

async function A(){}
async function B(){}
function C(){}
Promise.all([A(),B()]).then(C)

或者

const A = async () =>  await 'A';
const B = async () =>  await 'B';
const C = () =>  'C';
(async function All() {
    await Promise.all([A(), B()]);
    C();
})();

Promise.race

同样是将多个 Promise 包装成一个新的 Promise 实例。

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

注意点:

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

案例:

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.allSettled - ES2020

作用:用于确定一组异步操作是否都结束了(不管成功 fulfilled 或失败 rejected

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

注意点:

  • 1、只有等到参数数组的 所有 Promise 对象都发生状态变更(不管是 fulfilled 还是 rejected),返回的 Promise 对象才会发生状态变更

  • 2、该方法 返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled,不会变成rejected

  • 3、状态变成 fulfilled 后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的 每个 Promise 对象 - 对象的格式是固定的,如下

      // 异步操作成功时
      {status: 'fulfilled', value: value}
        
      // 异步操作失败时
      {status: 'rejected', reason: reason}
    

案例1:

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

const allSettledPromise = Promise.allSettled([resolved, rejected]); // 状态只可能变成 fulfilled

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

案例2:

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.any - ES2021

定义:接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。

注意点:

  • 1、只要参数实例 有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态
  • 2、如果所有参数实例 都变成 rejected 状态,包装实例就会变成 rejected 状态
Promise.any([
    fetch('https://v8.dev/').then(() => 'home'),
    fetch('https://v8.dev/blog').then(() => 'blog'),
    fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {  // 只要有一个 fetch() 请求成功
    console.log(first);
}).catch((error) => { // 所有三个 fetch() 全部请求失败
    console.log(error);
});