js三座大山之异步三promise本质

759 阅读8分钟

js三座大山

一:函数式编程
js三座大山之函数1 
js三座大山之函数2-作用域与动态this

二:面向对象编程
js三座大山之对象,继承,类,原型链

三:异步编程:
js三座大山之异步一单线程,event loope,宏任务&微任务
js三座大山之异步二异步方案
js三座大山之异步三promise本质
js三座大山之异步四-Promise的同步调用消除异步的传染性
js三座大山之异步五基于异步的js性能优化
js三座大山之异步六实现微任务的N种方式
js三座大山之异步七实现宏任务的N种方式

当前主流的js异步编程方案中 都是基于promise的。promise最大的优点就是统一了异步调用的api,所有异步操作都可以使用相同的模式来处理问题。使用链式调用,避免了回调地狱。本质上和回调的方案并没有区别。promise本质是一个存储回调的容器js三座大山之异步二异步方案 中的基于事件的发布订阅有点类似。

关于promise的基础使用请看
阮一峰promise
mdn-Promise

用法范式:

promise的用法可以分为两大步骤。

  1. 创建promise对象并传入执行器&同步运行执行器
const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onResolve(123);
  }, 2000);
};
const p = new Promise(execute);
  1. 注册异步回调函数,等待执行器返回结果,调用注册的函数。
// 成功的回调
p.then((data)=>{
  console.log('第一个成功的异步回调 异步结果:', data)
})
// 失败的回调
p.catch((reason)=>{
  console.log('第一个失败的异步回调 异步结果:', reason)
})
// 成功|失败都被执行的回调
p.finally(()=>{
  console.log('第一个finally异步回调')
})

容器结构:promise本质是一个存储回调的容器

那么这容器里面都有什么呐?

截屏2023-12-28 下午4.19.53.png

1.状态state

promsie内部维护了一个状态 可以从pendding->fulfilled || pendding->rejected。仅有这两种变化。且一旦状态改变,就不会再变,任何时候都可以得到这个结果。
这一点promise与基于事件的发布订阅不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

2.执行结果value

当执行器执行成功后需要设置结果 执行失败后需要设置失败原因 这都是运行的结果被保存在promise内部。

3.回调函数列表handles

有成功的回调函数 通过then,finally注册的 可以是多个
有失败的回调函数 通过then,catch,finally注册的 可以是多个

4.钩子函数onResolve/onReject

外部在promise构造函数中需要注入一个执行器 promise需要对外提供一个钩子 当执行器获取到异步结果后 需要通过钩子改变promise的状态 设置结果 触发回调函数执行。

5.api

例如接受注册回调函数的各种api then catch finally 以及一些静态race all resolve reject等。

promise特点

链式调用

promise的这些api 无论是静态的还是原型上的 都有一个特点即支持链式调用。实现链式调用的原理也很简单,每次都返回一个新的promise实例即可。

值穿透

promise的值通过钩子函数onResolve/onReject设置 或者通过注册的回调函数获取结果,如果处理函数返回一个非Promise值,那么这个值会被直接传递到下一个

举个例子

例子1:

const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onResolve(123);
  }, 2000);
};
const p = new Promise(execute);
p.then(result => {
    console.log('result:', result)
    return 456;
}).catch(error => {
    console.log('error:', error)
}).finally(() => {
    console.log('finally')
}).then(res=>{
    console.log('res:', res)
});
console.log('同步代码')

截屏2023-12-28 下午4.28.32.png

下面逐行分析下:

  1. js解析执行脚本代码,初始化执行器execute。
  2. 初始化第一个promise实例 将execute传入构造函数 execute被同步执行 遇到异步api计时器 加入到事件循环中
  3. p执行then 容器内部注册一个成功状态的回调函数 then执行完毕返回第二个promise。
  4. 第二个promise执行catch方法 内部被注册一个失败的回调函数 catch执行完毕 返回第三个promise实例。
  5. 第三个promise执行finally方法 内部注册成功和失败都要执行的回调函数 finally执行完毕 返回第四个promise实例。
  6. 第四个promise执行then方法 内部被注册一个成功的回调 返回第五个promise实例。
  7. 打印log 同步代码。 到这里同步代码执行完毕 实例化了5个promise。

下面是异步部分:

  1. 计时器运行结束 回调函数推入到事件队列 执行栈执行回调函数。
  2. resolve(123)钩子执行 触发第一个promise状态改变 pendding->fulfilled。value被设置为123。将value=123作为参数,依次执行容器内的成功回调函数。
  3. 成功回调函数执行结束 获取返回值456。触发第二个promise状态改变pendding->fulfilled,将第二个Promise的value设置为456。将value=456作为参数,依次执行第二个Promise内的成功回调函数。
  4. 第二个Promise内的成功回调函数为空。将value=456透传给第三个promise。同时第三个promise状态改变pendding->fulfilled。因为第三个promise内部的函数是通过finally注册的,所以在执行时并不会将value作为参数,仅仅是依次执行第三个Promise内的成功回调函数。
  5. 第三个Promise内的成功回调函数执行完毕,触发第四个promise状态改变pendding->fulfilled,同时将value=456透传给第四个promise. 将value=456作为参数,依次执行第四个Promise内的成功回调函数。
  6. 执行第四个Promise内的成功回调函数,获取返回值undefined,设置第五个promsie的value=undefined,触发第五个promise状态改变pendding->fulfilled。

promise值透传规律

  1. 通过钩子设置onResolve/onReject显示设置 例如上面的onResolve(123)
  2. 如果回调函数有返回值 则用当前的返回值设置下一个promsie的value 例如上面的返回值456
  3. 当没有对应的回调函数时(finally注册的相当于没有)透传当前的value。 例如第二个promise到第三个。

例子二:

const execute = (onResolve, onReject) => {
  setTimeout(() => {
    onReject('失败')
  }, 2000);
};
const p = new Promise(execute);
p.then((data)=>{
  console.log('成功的异步回调 异步结果:', data)
});
p.catch((reason)=>{
  console.log('失败的异步回调 异步结果:', reason)
  return 'fail'
});
p.finally(()=>{
  console.log('finally异步回调')
});

问题:运行结果是什么

截屏2023-12-28 下午3.52.53.png 打印失败和成功的回调应该不难理解 因为promsie被reject了。
但是为什么还会有报错呐?我明明已经catch了啊~

分析下:

截屏2023-12-28 下午4.34.44.png

  1. 首先同步代码执行结束 生成了4个promise实例。第一个promise内部被注册了三组回调函数
  2. 计时器运行结束 onReject钩子触发promise1状态改变pendding->rejected. promise的value='失败'。然后依次执行对应的回调列表。
  3. 执行第一个回调列表 发现没有失败对应的回调函数 所以透传当前的promise的值和状态给下一个promsie。所以promise2的状态改变pendding->rejected. promise的value='失败'。
  4. 执行第二个回调列表 有失败的处理函数 将promsie的value='失败'作为参数 执行此函数。得到返回结果’fail‘不是一个promise 因此将结果作为promise3的值 并更新状态pendding->fulfilled.
  5. 执行第三个回调列表 有失败的处理函数 但是这个函数是通过finally注册的 因此仅执行当前函数。并将当前promise的结果通过钩子onReject透传给promsie4。 所以第四个promsie状态pendding->rejected,value='失败'。
  • 第一次循环结束。
  1. 上次循环中3改变了promise2的状态为rejected,这次循环中依次执行对应的回调列表。因为promise2 没有回调列表 所以rejected状态,再次向上抛出。抛出第一个错误异常uncaught.
  2. 上次循环中promise3状态为fulfilled,内部没有回调列表 所以不执行。
  3. 上次循环中promise4状态为rejected,依次执行对应的回调列表,因为promise4没有回调列表 所以rejected状态,再次向上抛出。抛出第二个错误异常uncaught.

例子三

const execute = (resolve, reject) => {
  setTimeout(() => {
    resolve("123");
  }, 2000);
};
const p = new Promise(execute);
p.then((data) => {
  console.log("1111111");
})
  .finally(() => {
    console.log("33333333");
  })
  .finally(() => {
    console.log("55555555");
  });
p.finally(() => {
  console.log("2222222");
}).finally(() => {
  console.log("4444444");
});

输出结果:

1111111
2222222
33333333
4444444
55555555

代码实现:

手动实现了一版本promise,手写promise。有兴趣的同学可以看下~

一个思考题:

请问输出什么? 评论区告诉我答案 !

Promise.resolve().then(()=>{
  console.log(0)
  return Promise.resolve(4);
}).then((res)=>{
  console.log(res)
}).then(()=>{
  console.log(6)
})

Promise.resolve().then(()=>{
  console.log(1)
}).then(()=>{
  console.log(2)
}).then(()=>{
  console.log(3)
}).then(()=>{
  console.log(5)
})

参考

promise A+