18 Promise及其简易实现
参考文章:这一次,彻底弄懂 Promise 原理 - 对 Promise 源码进行了解析 - 没看完
30分钟,让你彻底明白Promise原理 - 真的看不进去,有需要再看吧
JS的异步编程模型,典型的如下图:
异步回调
Web 页面的单线程架构决定了异步回调,而异步回调影响到了我们的编码方式。
–> 将异步回调函数封装是一个很好的思维,
–> 但是封装后,如果遇到比较复杂的业务场景 嵌套了太多的回调函数 就很容易使得自己陷入了回调地狱
代码之所以看上去很乱,归结其原因有两点:
- 第一是 嵌套调用,下面的任务依赖上个任务的请求结果,并在上个任务的回调函数内部执行新的业务逻辑,这样当嵌套层次多了之后,代码的可读性就变得非常差了。
- 第二是任务的不确定性,执行每个任务都有两种可能的结果(成功或者失败),所以体现在代码中就需要对每个任务的执行结果做两次判断,这种对每个任务都要进行一次额外的 错误处理 的方式,明显增加了代码的混乱程度。
Promise 解决了什么问题呢?
作用:管理异步方式返回的代码
解决的问题:
- 第一是消灭嵌套调用 -> 可以实现链式的写法来实现异步操作
- 第二是合并多个任务的错误处理
Promise 优缺点
我们可以 注册任意数量的函数 在成功或者失败后运行,也可以 在任何时候注册 事件处理程序。
-
优点:
回调函数写成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现很多强大的功能。如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。
-
缺点:
1、无法取消
Promise
,一旦新建/声明它就会立即执行,无法中途取消2、“Promise会吃掉错误” - 如果不设置
cathch/reject
回调函数,Promise
内部抛出的错误,不会反应到外部 -> 不影响 Promise 外部脚本的执行3、当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
Promise 怎么解决嵌套回调问题
1、Promise
实现了 回调函数的延时绑定 -> 创建完 Promise
后再用 .then
方法绑定回调函数
2、将回调函数 onResolve
的返回值穿透到最外层 -> 需要通过返回值确认创建什么类型的Promise
任务
Promise 怎么处理异常
即使一个Promise
有多个回调函数,也可以通过一个 .catch
方法来捕获异常
–> Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被 onReject
函数处理或 catch
语句捕获为止
1/3、Promise 特点 -> 状态凝固
promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束。
resolve下面的语句其实是可以执行的,那么为什么 reject
的状态信息在下面没有接受到呢?
new
出一个 Promise
对象 - 初始化实例时,这个对象的起始状态就是 Pending
状态,在根据 resolve
或 reject
返回 Fulfilled
状态/ Rejected
状态 —> resolve 与 reject 只能执行一个
Promise
构造函数接受一个 函数作为参数,该函数的两个参数分别是 resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
new Promise((resolve, reject) => {
if (error) {
reject(new Error('Promise 异步执行异常'));
} else {
resolve('🌹');
}
}).then((res) => {
console.log(res); // 🌹
return '🦕🦕🦕';
}).then((res) => {
console.log(res); // 🦕🦕🦕
}).catch((error) => {
console.log(error); // Promise 异步执行异常
});
2/3、Promise 特点 - then
then
中返回了 新的 Promise
实例,但是 then
中注册的回调仍然是属于上一个 Promise 的;
then
函数是负责注册回调的,真正的执行是在 Promise
的状态被改变之后
【补充】:
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 的简单代码实现
因为定时器的执行效率不是太高,采用微任务实现了回调函数延迟绑定,这就是为什么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
方法指定的回调
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} // 案例 const resolved = Promise.resolve(42); const rejected = Promise.reject(-1); const allSettledPromise = Promise.allSettled([resolved, rejected]); allSettledPromise.then(results => console.log(results);); // 只有 resolved 的状态 // [ // { status: 'fulfilled', value: 42 }, // { status: 'rejected', reason: -1 } // ] // 使用 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);
});