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

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

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

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

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

免责声明

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

回调函数会执行并调用 resolve

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

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

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

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

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

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

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

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

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 的处理程序使用。

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

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

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

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

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 的哪些设计和行为?欢迎在本文下方自由言论,文明共享。

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

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

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

原文链接:https://juejin.cn/post/7355016460215337023 作者:前端暴走团

(0)
上一篇 2024年4月8日 上午10:16
下一篇 2024年4月8日 上午10:26

相关推荐

发表回复

登录后才能评论