🔥另一种角度解读JavaScript中的Promise、async和await 异步操作 (上)🔥

Promise 是一种编程模式,用于处理异步操作的结果或者在将来某个时间点产生的值。它可以帮助开发者更优雅地处理异步编程,使代码更易于阅读、维护和理解。除了 JavaScript,许多其他编程语言也引入了类似 Promise 的概念来处理异步操作,例如:

  • Python: 使用 asyncawait 关键字来实现类似 Promise 的异步编程模式。
  • Java: 引入了 CompletableFuture 类来处理异步操作和组合多个异步操作。
  • C#: 引入了 Taskasync/await 关键字来处理 异步编程。
  • Swift: 使用 asyncawait 关键字来实现异步操作。
  • Rust: 提供了 std::futureasync/await 来支持异步编程。

关于 Promise 的标准,最初是由 JavaScript 社区提出的,后来在 ECMAScript 6(ES6)规范中正式纳入了 Promise 对象。其实Promise 规范有很多,如Promise/APromise/BPromise/D 以及 Promise/A 的升级版 Promise/A+ES6 中采用了 Promise/A+ 规范。

(注:Promise/A+规范不包含raceallcatch等方法的说明,这些实现是ES6自定义的规范)

JavaScript 中,Promise 是一种内置的对象类型,用于处理异步操作。一个 Promise 可以处于以下三种状态之一:

  • Pending(进行中):初始状态,异步操作尚未完成也没有被拒绝。
  • Fulfilled(已完成):异步操作成功完成,并返回一个值。
  • Rejected(已拒绝):异步操作失败,并返回一个错误。

在解析 Promise 内部原理之前,先来看几个有关代码运行顺序的问题:

Promise 代码执行顺序

  • PS:第一问
js 复制代码
new Promise((res,rej)=>{
    console.log(111);
    res(222)
}).then(res=>{
    console.log(res);
    return 444
}).then(res=>{
    console.log(res)
    return  555
}).then(res=>{
    console.log(res);
    return 555
}).then(res=>{
    console.log(res)
});


Promise.resolve(333).then(res=>{
    console.log(res)
    return 'aaa'
}).then(res=>{
    console.log(res)
    return 'bbb'
}).then(res=>{
    console.log(res)
    return 'ccc'
}).then(res=>{
    console.log(res)
    return 'ddd'
})

输出顺序为:111 222 333 444 aaa 555 bbb 555 ccc

是不是太简单了,继续:

  • PS:第二问
js 复制代码
new Promise(resolve => {
  resolve(
    new Promise(resolve => {
      resolve(1);
    })
  );
}).then(res => {
  console.log('1');
});

new Promise(resolve => {
  resolve(2);
})
  .then(() => {
    console.log('2');
  })
  .then(() => {
    console.log('3');
  })
  .then(() => {
    console.log('4');
  });

是不是想说 output: 1 2 3 4

实则输出顺序为:1 3 2 4

是不是有点迷糊了?不着急,后面会说,继续看下一个:

  • PS:第三问
js 复制代码
Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

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

是不是也不太清楚? 别着急,后面会讲解

输出顺序为:0 1 2 3 4 5 6

根据以上三问,我们接下来逐一讲解:

第一问:

自然不必说,只是需要注意几点:

  • Promise 状态一旦改变就永远不会变化
  • 推送进入微任务队列的不是 resove(value)reject(error) 而是以微任务的队列执行的 then 中的回调函数,该回调函数是在 resove(value)reject(error) 执行获取到"结果"之后立即进入的

所以第一问中是微任务交替进行输出

第二问:

和第一问不一样的地方是开始的 Promiseresolve 的参数值是一个 Promise 类型的数据,也就是 new Promise(resolve => { resolve(1); }) 这段代码,如何执行这段代码,这里我们必须要说的就是 ECMA 规范,

注意:你可能看过 c++ 编写的的V8 引擎中关于 Promise 的源码,有兴趣的小伙伴可以自行了解,这里我们不做过多探讨。而是从 ECMA 规范角度来看,为什么输出是这样,以及为何这样设计。

首先是 ECMA 规范中把 Promise 的微任务分成了两种类型,NewPromiseReactionJobNewPromiseResolveThenableJob

NewPromiseReactionJob

Promise 确定状态时执行 then() 中注册的回调,会产生这种微任务。也就是:当前 Promise 状态已经改变,接着调用了 then(),例如:

js 复制代码
Promise.resolve(999).then(res => console.log(res));

res => console.log(res) 该回调函数就处在这种微任务之中。规范中对于 Promise.prototype.then 的描述如下:

假设状态改变为 fulfilled ,继续查看 PerformPromiseThen 中对 fulfilled 状态的处理:

由此可见这里创建了一个 NewPromiseReactionJob 微任务,并将其加入到了微任务队列中,继续查看NewPromiseReactionJob 内部是如何执行的:

这里该微任务主要做如下处理:

  • NewPromiseReactionJob 接受参数 reaction(一个 PromiseReaction 记录)和参数(一个 ECMAScript 语言值),并返回一个带有字段 [[Job]] (一个闭包)和 [[Realm]](记录值或null)的记录 。 它返回一个新的闭包,将适当的处理程序应用于传入值,并使用处理程序的返回值来完成或拒绝与该处理程序关联的派生 Promise; 上述过程简要总结为:
    • 执行 handler 闭包,handler 就是 then() 中注册的回调函数,得到其返回结果。
    • then() 中产生的新 Promise 执行 resolve(返回结果) 或 reject(返回结果)。

NewPromiseResolveThenableJob

这个微任务就是本文的重点,上述的 NewPromiseReactionJob 微任务是平时 Promise 执行的基本操作,首先看一下规范介绍:

这里是 resolve 函数执行的规范:

这里表明,如果一个 对象数据的 then 属性是可以调用执行(也即是它是一个 Function ),那么此时这个对象就是被称作thenable对象。调用 resolve() 传递的参数值如果是一个 thenable 对象,就会产生 NewPromiseResolveThenableJob 这种微任务了。 举个例子说明一下:

  • 先写一段 自定义的 Promise 代码,这里使用 queueMicrotask api 模拟微任务
js 复制代码
const _my_promise = function(executor){
    let  value = null;
    const res = (v) =>{
        value = v;

    }
    const rej = (e) =>{
        value = e;

    }
    this.then= function (onFulfilled, onRejected) {
        queueMicrotask(() => {
        console.log(value,"pre_load")
        })

    }
    try {
        executor(res, rej);
    } catch (err) {
        rej(err);
    }
}
  • 举例说明例子二:
js 复制代码
new Promise(resolve => {
  resolve(
    new _my_promise(resolve => {
      resolve(4);
    })
  );
}).then(res => {
  console.log(res);
});

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

看一下输出: 接下来看看这个微任务的内容:1 4 2 'pre_load' 3 5 6

  • 举例说明例子三:
js 复制代码
Promise.resolve().then(() => {
    console.log(0);
    return new _my_promise((res,rej)=>{
        res(4)
    });
}).then((res) => {
    console.log(res)
})

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

看一下输出: 接下来看看这个微任务的内容:0 1 2 4 'pre_load' 3 5 6

很明显。这里我并没有执行 then 函数,为什么例二和例三依然输出 console.log(value,"pre_load") 这句话呢?没错,就是因为自定义 _my_promise 对象是一个 thenable 对象,因为具有回调函数 then,接下来看一下 这个微任务执行的内容

也即是在此产生了如同下式的代码,也就是执行了 then 的回调函数

注:resolve 中参数值为 Promise 和 then 的回调函数中 然绘制为 Promise 对象实质上是一样的,走的微任务路线相似,所以例二和例三可以放在一起理解

js 复制代码
thenable.then(resolve, reject);

这意味着什么呢?也就是说明以下两段代码等价:

js 复制代码
其一:
Promise.resolve().then(() => {
    console.log(0);
    return new Promise((res,rej)=>{
        res(4)
    });
}).then((res) => {
    console.log(res)
})

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

其二:
Promise.resolve().then(() => {
    console.log(0);
    return new Promise((res,rej)=>{
        res(4)
    }).then(res => res);   // 多加一个 then 不影响最后执行顺序,但是如果此时再加一个then,就会再多出一个微任务,意义不相同
}).then((res) => {
    console.log(res)
})

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

但是为什么和原生 Promise 也即是例子三的输出结果不一致呢? 这就是因为,如果 thenable 对象是一个 Promise 对象,那么这个微任务执行之后又会产生一个新的微任务,既是例三中,return new Promise((res,rej)=>{ res(4) }); 这里产生了两次微任务,那么为什么要这样,是设计的缺陷还是有何目的呢?看一下规范是如何说的:

直译就是:

此Job使用提供的 thenable 及其 then 方法来解决给定的 Promise。 此过程必须作为Job进行,以确保 then 方法的状态转换发生在任何周围代码的状态转换完成之后。

注意:这里应该是想说要等周围的同步代码执行完后才会执行这个,因为 thenable 对象的不一定是 Promise 实例,也可能是用户创建的任何对象;如果这个对象的 then 是同步方法,那么这样做就可以保证 then 的执行顺序也是在微任务中。

PS:走马观花式的看一小部分所谓的"V8底层源码",并不意味着你能了解其设计理念和思想,不要遇到问题就拿源码说事。因为很多人并没有精力和耐心去了解这些东西,甚至是 ECMA 规范也不见得别人想了解。当然,有时了解源码固然有必要,但是提问的人真的是想听你述说底层源码的事情吗?或者该阶段的开发者目前所涉及的领域对于这个问题需要理解到这种程度吗?不一定,大部分开发者也许只想要解决实际问题,或得到一个相对满意的答复,仅此而已。以上只是个人见解。

相关推荐
一个处女座的程序猿O(∩_∩)O34 分钟前
vue 如何实现复制和粘贴操作
前端·javascript·vue.js
赔罪1 小时前
HTML-列表标签
服务器·前端·javascript·vscode·html·webstorm
谦谦橘子1 小时前
手写React useEffect方法,理解useEffect原理
前端·javascript·react.js
九州~空城1 小时前
C++中map和set的封装
java·前端·c++
H轨迹H1 小时前
DVWA靶场JavaScript Attacks漏洞low(低),medium(中等),high(高),impossible(不可能的)所有级别通关教程
javascript·网络安全·渗透测试·dvwa·web漏洞
椒盐大肥猫1 小时前
axios拦截器底层实现原理
前端·javascript
夕水2 小时前
我的2024-人生须为有益事
前端·年终总结
明月看潮生2 小时前
青少年编程与数学 02-006 前端开发框架VUE 08课题、列表渲染
前端·javascript·vue.js·青少年编程·编程与数学
前端要努力2 小时前
30而立,月哥的2024年终总结,小亏几百万
前端·后端·面试
hawk2014bj2 小时前
Vue3 中的插槽
前端·javascript·vue.js