Web/06-JavaScript基础:异步编程/09-Promise类的方法.md
2023-05-22 20:59:21 +08:00

20 KiB
Raw Blame History

title
09-Promise类的方法

Promise 类的方法简介

Promise 的 API 分为两种:

  • Promise 实例的方法也称为Promis的实例方法
  • Promise 类的方法也称为Promise的静态方法

前面几篇文章,讲的都是 Promise 实例的方法它们都是存放在Promise的prototype上的。今天这篇文章我们来讲一下 Promise 的方法。

Promise 的方法:可以直接通过大写的Promise.xxx调用的方法。这里的xxx就称之为静态方法。

Promise 的自带 API 提供了如下静态方法:

  • Promise.resolve()
  • Promise.reject()
  • Promsie.all():并发处理多个异步任务,所有任务都执行成功,才算成功(走到 resolve只要有一个失败就会马上走到 reject整体都算失败。
  • Promise.race():并发处理多个异步任务,返回的是第一个执行完成的 promise且状态和第一个完成的任务状态保持一致。
  • Promise.allSettled():并发处理多个异步任务,返回所有任务的执行结果(包括成功、失败)。当你有多个彼此不依赖的异步任务执行完成时,或者你想知道每个 promise 的结果时,通常使用它。
  • Promise.any()

Promise.resolve() 和 Promise.reject()

使用场景

当我们在定义一个 Promise 的过程中,如果涉及到异步操作,那就需要通过new Promise的方式创建一个 Promise 实例。

但有些场景下,我们已经有一个现成的内容了,希望将其转成 Promise 来使用。此时,我们可以用 Promise.resolve() 将其封装为成功的状态。同理,用Promise.reject()可以封装为失败的状态。

比如说有时候promise 里面并没有异步操作,我只是单纯地想通过 promise 的方式返回一个字符串(有的业务就是有这样的需求),那就可以通过 Promise.reslove('字符串')Promise.reject('字符串') 这种简写的方式返回。

代码举例:

const promise = Promise.resolve('qianguyihao')

promise.then(res => {
  console.log('res:', res);
});

// 上方代码如果是连续书写的话,也可以简写成:
Promise.resolve('qianguyihao').then(res => console.log('res:', res));

Promise.resolve('qianguyihao') 这种写法似乎过于啰嗦,直接 return 'qianguyihao'不行吗that depands。举个例子我们在调用别人的方法时对方如果要求返回值必须是 Promise对象那么Promise.resolve() 就能派上用场了。

Promise.resolve()Promise.reject()的返回值就是一个 Promise。

用法拆解

Promise.resolve()的用法相当于new Promise()并执行resolve()操作。下面这两种写法是等价的:

// 写法1Promise 类的 resolve() 方法
const promise = Promise.resolve(params);

// 写法2Promise 实例的 resolve() 方法
const promise = new Promise((resolve, reject)=> resolve(params));

Promise.reject()的用法同理。下面这两种写法是等价的:

// 写法1Promise 类的 reject() 方法
const promise = Promise.reject(params);

// 写法2Promise 实例的 reject() 方法
// 第一个形参用不到,我们通常用 下划线 表示。这是一种约定俗成的规范写法。
const promise = new Promise((_, reject)=> reject(params));

写法2显然过于啰嗦写法1用得更多。

写法2中我们可以学到一个写代码的小技巧如果某个形参我们用不到但又必须写出来的话我们通常用下划线表示。这是一种约定俗成的规范写法,比较简洁。

resolve()的参数

resolve()参数中传入的值,可以有很多种类型,进而决定 Promise 的状态:

  • 情况1如果resolve()中传入普通的值或者普通对象那么这个值会作为then()回调的参数。Promise 的状态为fulfilled。
  • 情况2如果resolve()中传入的是另外一个新的 Promise,那么原 Promise 的状态将交给新的 Promise 决定
  • 情况3如果resolve()中传入的是thenable 对象,那就会执行该then()方法,并且根据then()方法的结果来决定Promise的状态

我们在前面的文章《Promise入门详解》中针对这三种情况做了详细介绍在此不再赘述。

reject()的参数中无论传入什么值Promise都会直接进入 rejected 状态,并触发 catch() 方法的执行。

代码详解

resolve()、reject()既可以作为 Promise 实例的方法,也可以作为 Promise 类的方法。这两种情况,我们来对比看看。

例 1

function foo(flag) {
    if (flag) {
        return new Promise((resolve) => {
            // 这里可以做异步操作
            resolve('success');
        });

        // return Promise.resolve('success2');
    } else {
        return new Promise((reslove, reject) => {
            // 这里可以做异步操作
            reject('fail');
        });
    }
}

// 执行 reslove 的逻辑
foo(true).then((res) => {
    console.log(res);
});

// 执行 reject 的逻辑
foo(false).catch((err) => {
    console.log(err);
});

例 2见证奇迹的时刻

function foo(flag) {
    if (flag) {
        // Promise的静态方法直接返回字符串
        return Promise.resolve('success');
    } else {
        // Promise的静态方法直接返回字符串
        return Promise.reject('fail');
    }
}

// 执行 reslove 的逻辑
foo(true).then((res) => {
    console.log(res);
});

// 执行 reject 的逻辑
foo(false).catch((err) => {
    console.log(err);
});

例 1 和例 2 的打印结果是一样的。这两段代码的区别在于:例 1 里面可以封装异步任务;例 2 只能单纯的返回一个字符串等变量,不能封装异步任务。

Promise.all()

Promise.all()的参数是一个数组,数组里可以填写多个 Promise。比如 Promsie.all([p1, p2, p3])。它的作用是将p1、p2、p3 等多个 Promise 包裹在一起,组成一个新的 Promise

新 Promise 的状态由 p1、p2、p3 所在数组里所有的 Promse 共同决定

  • 当 p1、p2、p3等所有的 Promise 状态变为 fulfilled 时,新 Promise 的状态变为 fulfilled并且会将p1、p2、p3 等所有Promise的返回值组成一个数组。
  • 当p1、p2、p3 等 Promise中有一个 Promise 状态为 rejected()时,新 Promise 的状态变为 rejected并且会将第一个reject的返回值作为catch() 的参数。

Promsie.all([p, p2, p3])`的使用场景:并发处理多个异步任务,所有任务都执行成功,才算成功(才会走到 then只要有一个任务失败就会马上走到 catch整体都算失败。参数里传的是多个 Promise 实例组成的数组。

语法举例

1、异步任务都执行成功时

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise1');
        resolve('promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise2');
        resolve('promise 2 成功');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise3');
        resolve('promise 3 成功');
    }, 3000);
});

Promise.all([promise1, promise2, promise3])
    .then((res) => {
        // 三个异步任务都执行成功,才会走到这里
        // 这里拿到的 res是三个成功的返回结果组成的数组
        console.log('res:' + JSON.stringify(res));
    })
    .catch((err) => {
        // 只要有一个异步任务执行失败,就会马上走到这里
        console.log(err);
    });

打印结果:

// 1秒后
执行 promise1

// 2秒后
执行 promise2

// 3秒后
执行 promise3

res:["promise 1 成功","promise 2 成功","promise 3 成功"]

2、异步任务有至少一个执行失败时

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise1');
        resolve('promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise2');
        // 这里通过 reject() 的方式,表示任务执行失败
        reject('promise 2 失败');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise3');
        resolve('promise 3 成功');
    }, 3000);
});

Promise.all([promise1, promise2, promise3])
    .then((res) => {
        // 三个异步任务都执行成功,才会走到这里
        console.log('走到 then:' + JSON.stringify(res));
    })
    .catch((err) => {
        // 只要有一个异步任务执行失败,就会马上走到这里
        console.log('走到 catch:' + err);
    });

打印结果:

// 1秒后
执行 promise1

// 2秒后
执行 promise2
走到 catch:promise 2 失败

// 3秒后
执行 promise3

可以看到,当 promise2 执行失败之后,马上就走到了 catch而且 promise3 里的 resolve 并没有执行。

Promise.all()案例:多张图片上传

比如说,现在有一个图片上传的接口,每次请求接口时只能上传一张图片。需求是:当用户连续上传完九张图片(正好凑齐九宫格)之后,给用户一个“上传成功”的提示。这个时候,我们就可以使用Promsie.all()

1、代码举例如下

const imgArr = ['1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg', '8.jpg', '9.jpg'];
const promiseArr = [];
imgArr.forEach((item) => {
    const p = new Promise((resolve, reject) => {
        // 在这里做图片上传的异步任务。图片上传成功后,接口会返回图片的 url 地址
        //  upload img ==> return imgUrl
        if (imgUrl) {
            // 单张图片上传完成
            resolve(imgUrl);
        } else {
            reject('单张图片上传失败');
        }
    });
    promiseArr.push(p);
});
Promise.all(promiseArr)
    .then((res) => {
        console.log('图片全部上传完成');
        console.log('九张图片的url地址组成的数组' + res);
    })
    .catch((res) => {
        console.log('部分图片上传失败');
    });

2、上方代码解释

1只有九张图片都上传成功才会走到 then。

2按时间顺序来看假设第一张图片上传成功第二张图片上传失败那么最终的表现是

  • 对于前端来说,九张图都会走到 reject整体会走到 catch不会走到 then。

  • 对于后端来说,第一张图片会上传成功(因为写入 DB 是不可逆的),第二张图上传失败,剩下的七张图,会正常请求 upload img 接口。

3、特别说明

  • 第一张图会成功调 upload 接口,并返回 imgUrl但不会走到 resolve因为要等其他八张图的执行结果再决定是一起走 resolove 还是一起走 reject。

  • 当执行 Promise.all() / Promise.race() / Promise.any() 的时候,其实九张图的 upload img 请求都已经发出去了。对于后端来说,是没有区别的(而且读写 DB 的操作不可逆),只是在前端的交互表现不同、走到 resolve / reject / then / catch 的时机不同而已。

上面这个例子,在实际的项目开发中,经常遇到,属于高频需求,需要记住并理解。

4、思维拓展

  • 拓展 1如果你希望九张图同时上传并且想知道哪些图上传成功、哪些图上传失败则可以这样做无论 upload img 接口请求成功与否,全都执行 resolve。这样的话,最终一定会走到 then然后再根据接口返回的结果判断九张图片的上传成功与否。

  • 拓展 2实战开发中在做多张图片上传时可能是一张一张地单独上传各自的上传操作相互独立。此时 Promise.all便不再适用,这就得具体需求具体分析了。

Promise.race()

Promise.race([p1, p2, p3]):并发处理多个异步任务,返回的是第一个执行完成的 promise且状态和第一个完成的任务状态保持一致。参数里传的是多个 promise 实例组成的数组。

上面这句话,第一次读时,可能很绕口。我说的再通俗一点:在多个同时执行的异步任务中,先找出哪个异步任务最先执行完成(无论是走到 resolve还是走到 reject都算执行完成整体的状态就跟这个任务保持一致。如果这个任务执行成功那整体就算成功走到 then如果这个任务执行失败那整体就算失败走到 catch

race的中文翻译,可以理解为“竞赛”。意思是,谁先抢到名额,就认定谁了。无论这个人最终的结局是成功或者失败,整体的结局,都以这个人的结局为准。

我刚开始学 Promise.race()的时候,误以为它的含义是“只要有一个异步执行成功,整体就算成功(走到 then所有任务都执行失败整体才算失败走到 catch”。现在想来真是大错特错过于懵懂。

现在我顿悟了准确来说Promise.race()强调的是:只要有一个异步任务执行完成,整体就是完成的。

Promise.race()的应用场景:在众多 Promise 实例中,最终结果只取一个 Promise谁返回得最快就用谁的 Promise

我们来看看各种场景的打印结果,便能擦干泪水,继续前行。

语法举例

场景 1、所有任务都执行成功时

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise1');
        resolve('promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise2');
        resolve('promise 2 成功');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise3');
        resolve('promise 3 成功');
    }, 3000);
});

Promise.race([promise1, promise2, promise3])
    .then((res) => {
        // 第一个完成的任务,如果执行成功,就会走到这里
        // 这里拿到的 res是第一个成功的 promise 返回的结果,不是数组
        console.log(JSON.stringify(res));
    })
    .catch((err) => {
        // 第一个完成的任务,如果执行失败,就会走到这里
        console.log(err);
    });

打印结果:

// 1秒后
执行 promise1
"promise 1 成功"

// 2秒后
执行 promise2

// 3秒后
执行 promise3

场景 2、第一个任务成功、第二个任务失败时

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise1');
        resolve('promise 1 成功');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise2');
        // 第二个任务执行失败时
        reject('promise 2 失败');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise3');
        resolve('promise 3 成功');
    }, 3000);
});

Promise.race([promise1, promise2, promise3])
    .then((res) => {
        // 第一个完成的任务,如果执行成功,就会走到这里
        console.log('走到then:' + res);
    })
    .catch((err) => {
        // 第一个完成的任务,如果执行失败,就会走到这里
        console.log('走到catch:' + err);
    });

打印结果:

// 1秒后
执行 promise1
走到then:promise 1 成功

// 2秒后
执行 promise2

// 3秒后
执行 promise3

可以看出,场景 2 的打印结果和场景 1 的打印结果,是一样的。因为最新执行完成的任务,是成功的,所以整体会马上走到 then且整体就算成功。

场景 3、第一个任务失败、第二个任务成功时

const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise1');
        // 第一个任务执行失败时
        reject('promise 1 失败');
    }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise2');
        resolve('promise 2 成功');
    }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log('执行 promise3');
        resolve('promise 3 成功');
    }, 3000);
});

Promise.race([promise1, promise2, promise3])
    .then((res) => {
        // 第一个完成的任务,如果执行成功,就会走到这里
        console.log('走到then:' + res);
    })
    .catch((err) => {
        // 第一个完成的任务,如果执行失败,就会走到这里
        console.log('走到catch:' + err);
    });

打印结果:

// 1秒后
执行 promise1
走到catchpromise 1 失败

// 2秒后
执行 promise2

// 3秒后
执行 promise3

看清楚了没?场景 3 的最终打印结果,是走到了 catch任务 2 和任务 3 里的 resolve并没有继续执行。

场景 3 的代码,一定好好好理解。

Promise.race()举例:图片加载超时

现在有个需求是这样的:前端需要加载并显示一张图片。如果图片在三秒内加载成功,那就显示图片;如果三秒内没有加载成功,那就按异常处理,前端提示“加载超时”或者“请求超时”。

代码实现:

// 图片请求的Promise
function getImg() {
    return new Promise((resolve, reject) => {
        let img = new Image();
        img.onload = function () {
            // 图片的加载,是异步任务
            resolve(img);
        };
        img.src = 'https://img.smyhvae.com/20200102.png';
    });
}

// 加载超时的 Promise
function timeout() {
    return new Promise((resolve, reject) => {
        // 采用 Promise.race()之后,如果 timeout() 的 promise 比 getImg() 的 promise先执行说明定时器时间到了那就算超时。整体的最终结果按失败处理。
        setTimeout(() => {
            reject('图片加载超时');
        }, 3000);
    });
}

Promise.race([getImg(), timeout()])
    .then((res) => {
        // 图片加载成功
        console.log(res);
    })
    .catch((err) => {
        // 图片加载超时
        console.log(err);
    });

如代码注释所述:采用 Promise.race() 之后,如果 timeout() 的 promise 比 getImg() 的 promise 先执行,说明定时器时间到了,那就算超时。整体的最终结果按失败处理。

这个思路很巧妙。用同样的思路,我们还可以处理网络请求超时的问题。如果接口请求时长超过 3 秒,就按超时处理,也就是下面我们要举的例子。

Promise.race()举例:网络请求超时

现在有这种需求:如果接口请求时长超过 3 秒,就按超时处理。

基于这种需求,我们可以用 Promise.race() 来实现:一个 Promise 用于 请求接口,另一个 Promise 用于执行 setTimeout()。把这两个 Promise 用 Promise.race()组装在一起,谁先执行,那么最终的结果就以谁的为准。

代码举例:

function query(url, delay = 4000) {
    let promiseArr = [
        myAajax(url),
        new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('网络请求超时');
            }, delay);
        }),
    ];
    return Promise.race(promiseArr);
}

query('http://localhost:8899/xxx_url', 3000)
    .then((res) => {
        console.log(res);
    })
    .catch((error) => {
        console.log(error);
    });

总结

Promise 不仅能解决嵌套异步任务的回调地域问题,也可做多个异步任务的并发请求,还可以进行舒适简洁的状态管理。

Promise 本身不是异步的,但是它可以封装异步任务,并对异步操作进行良好的状态管理,这便是 Promise 的魅力所在。

赞赏作者

创作不易,你的赞赏和认可,是我更新的最大动力: