Promise 可视化:幕后机制和执行过程

《前端暴走团》,喜欢请抱走~大家好,我是团长林语冰。

JS 中的 Promise 乍一看似乎有点望而生畏,但只要深度学习 Promise 幕后的工作机制,我们就可以让它们变得更通俗易懂。

不久前,海外一位前端小姐姐制作了一个 Promise 执行过程可视化的视频教程和博客。在本文中,我们会深度学习 Promise 的内部工作原理,并探讨 Promise 如何在 JS 中赋能异步非阻塞任务。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 JavaScript Visualized: Promise Execution

创建 Promise 的方式之一,就是使用 new Promise 构造函数,它接收一个执行器函数,该执行器可以传递 resolvereject 参数。

js 复制代码
new Promise((resolve, reject) => {
  // 待办任务:可能是某些异步操作
})

当调用 Promise 构造函数时,内部会发生某些事情:

  • 创建一个 Promise 实例对象 ,该对象包含多个内部插槽,包括但不限于:
    • [[PromiseState]]
    • [[PromiseResult]]
    • [[PromiseIsHandled]]
    • [[PromiseFulfillReactions]]
    • [[PromiseRejectReactions]]
  • 创建一个 Promise 容器记录 ,这"封装"了该实例对象,并添加了某些额外功能,来完成或拒绝该实例。这些函数会控制 Promise 的最终状态 [[PromiseState]] 和结果 [[PromiseResult]],并启动异步任务。

粉丝请注意,在 ES 语言说明书(ECMAScript Language Specification),[[]] 双重方括号表示外部用户不可见的内部插槽,大家可以简单理解为类似于 JS 中的类私有属性。

我们可以调用 resolve 来解析这个 Promise,这可以通过执行器函数来做到。当我们调用 resolve 时:

  1. [[PromiseState]] 会被设置为 "fulfilled"完成状态
  2. [[PromiseResult]] 会被设置为我们传递给 resolve 的值,在这个例子中结果就是 "Done!"

调用 reject 的过程同理可得,之后 [[PromiseState]] 会被设置为 "rejected" 拒绝状态,且 [[PromiseResult]] 结果会被设置为我们传递给 reject 的值,即 "Fail!"

这简直棒棒哒......但是,使用函数来更改对象的某些内部属性有什么值得大精小怪吗?

答案就藏在我们迄今为止跳过的两个内部插槽相关的行为中:

  • [[PromiseFulfillReactions]] 完成响应
  • [[PromiseRejectReactions]] 拒绝响应

[[PromiseFulfillReactions]] 字段包含了 Promise 响应 。该对象是通过将 then 处理程序链接到 Promise 创建的。

除了其他字段外,这个 Promise 响应 还包含一个 [[Handler]] 处理程序属性,该属性保存了我们传递给 then 的回调函数。当 Promise 解析时,该处理程序会被添加到微任务队列 中,且有权读写 Promise 解析相关的值。

Promise 解析时,该处理程序接收 [[PromiseResult]] 结果的值作为其参数,然后将其推送到微任务队列中。

这就是 Promise 异步功能的用武之地!

微任务队列 是事件循环中的专属队列。当调用栈 为空时,事件循环首先处理微任务队列 中等待的微任务,然后再处理常规任务队列中的宏任务,任务队列也被称为"回调队列"或"宏任务队列"。

举一反一,我们可以创建一个 Promise 响应记录 ,通过链接 catch 来处理 Promise 的拒绝。当 Promise 被拒绝时,该回调函数会被添加到微任务队列中。

到目前为止,我们只在执行器函数中直接调用了 resolvereject。尽管这是合法的,但它并没有充分榨干 Promise 的全部力量和主要目标!

大多数情况下,我们期望 resolvereject 在稍后的某个时间点调用,通常是在异步任务完成时。

异步任务发生在主线程之外,比如读取文件 fs.readFile、发出网络请求 https.getXMLHttpRequest,或简单的计时器 setTimeout

当这些任务在未来某个未知时刻完成时,我们可以使用回调函数,比如此类异步操作通常提供来 resolve 我们从异步任务中取回的数据,以及当报错时 reject

为了可视化这一点,让我们逐步完成执行过程。为了使这个演示简单粗暴接地气,我们会使用 setTimeout 添加某些异步行为。

js 复制代码
new Promise(resolve => {
  setTimeout(() => resolve('Done!'), 100)
}).then(result => console.log(result))

首先,new Promise 构造函数会添加到调用栈 中,并创建 Promise 对象

然后,执行执行器函数。在函数体内的第一行,我们调用了 setTimeout,它会被添加到调用栈中。

setTimeout 负责调度 Web API 中的定时器,延迟 100 毫秒,之后我们传递给 setTimeout 的回调函数会被推送到任务队列

这里的异步行为与 setTimeout 有关,但与 Promise 无关。我举个栗子只是为了表演使用 Promise 的常见方式,即在延迟一段时间后解析 Promise

虽然但是,延迟本身并不是 Promise 造成的。Promise 旨在与异步操作强强联手,但这些异步操作可以来自不同的 API,比如计时器或网络请求。

在计时器和构造函数从调用栈 中弹出后,引擎会遭遇 then

then 会被添加到调用栈 中,并创建一个 Promise 响应 记录,该处理程序是我们作为回调传递给 then 处理程序的代码。

由于 [[PromiseState]] 仍然是 "pending" 状态,因此该 Promise 响应 记录会添加到 [[PromiseFulfillReactions]] 完成响应的列表中。

100 毫秒后,setTimeout 的回调函数会被推送到任务队列中。

此时整个脚本已经运行完毕,因此调用栈 为空,这意味着,该任务现在会从任务队列 转移到调用栈上。

回调函数会执行并调用 resolve

调用 resolve 会将 [[PromiseState]] 设置为 "fulfilled" 状态,将 [[PromiseResult]] 设置为 "Done!" 结果,以及与 Promise 响应 关联的处理程序会被添加到微任务队列中。

resolve 和回调函数会从调用栈中弹出。

由于此时调用栈 为空,事件循环会优先检查微任务队列 ,其中 then 处理程序的回调函数正在等待。

回调函数现在已经添加到 调用栈 中,并打印 result 的值,即 [[PromiseResult]] 的结果值 ------ 字符串 "Done!"

一旦回调函数执行完毕,并从调用栈中弹出,程序就码到功成了!

除了创建 Promise 响应 之外,then 还返回一个 Promise。这意味着,我们可以将多个 then 相互链接,举个栗子:

js 复制代码
new Promise(resolve => {
  resolve(1)
})
  .then(result => result * 2)
  .then(result => result * 2)
  .then(result => console.log(result))

执行此代码时,会在调用 Promise 构造函数时创建 Promise 对象。之后,每当引擎遭遇 then 时,都会创建 Promise 响应记录和 Promise 对象。

在这两种情况下,then 回调都会将接收到的 [[PromiseResult]] 乘以 2 的值。then[[PromiseResult]] 设置为此计算的结果,该结果又由下一个 then 的处理程序使用。

最终,结果被记录。最后一个 thenPromise[[PromiseResult]]undefined,因为我们没有显式返回值,这意味着它隐式返回 undefined

当然啦,计算数字并不是现实开发的常见场景。相反,我们可能想逐步更改 Promise 的结果,比如逐步更改图像的外观。

举个栗子,我们可能需要采取一系列增量步骤,通过调整大小、应用滤镜、添加水印等操作,从而修改图像的外观。

js 复制代码
function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => resolve(img)
    img.onerror = reject
    img.src = src
  })
}

loadImage(src)
  .then(image => resizeImage(image))
  .then(image => applyGrayscaleFilter(image))
  .then(image => addWatermark(image))

这些类型的任务通常涉及异步操作,这使得 Promise 成为以非阻塞方式管理异步任务的最佳实践。

高潮总结

简而言之,Promise 只是具备某些更改其内部状态的附加功能的对象。

Promises 的一个牛逼之处在于,如果通过 thencatch 附加处理程序,它可以触发异步操作。由于处理程序被推送到微任务队列,我们可以以非阻塞方式处理最终结果。这使得处理错误、将多个操作链接在一起变得轻而易举,并使代码更具可读性和可维护性!

Promise 仍然是一个基础概念,对于每个 JS 开发者而言都至关重要。

本期话题是 ------ 你最喜欢、或最不能接受 Promise 的哪些设计和行为?欢迎在本文下方自由言论,文明共享。

坚持阅读,自律打卡,每天一次,进步一点。

《前端暴走团》,喜欢请抱走!我是团长林语冰。谢谢大家的点赞,掰掰~

相关推荐
Myli_ing9 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维26 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
I_Am_Me_40 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁1 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue