《前端暴走团》,喜欢请抱走~大家好,我是团长林语冰。
JS 中的 Promise
乍一看似乎有点望而生畏,但只要深度学习 Promise
幕后的工作机制,我们就可以让它们变得更通俗易懂。
不久前,海外一位前端小姐姐制作了一个 Promise
执行过程可视化的视频教程和博客。在本文中,我们会深度学习 Promise
的内部工作原理,并探讨 Promise
如何在 JS 中赋能异步非阻塞任务。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 JavaScript Visualized: Promise Execution。
创建 Promise
的方式之一,就是使用 new Promise
构造函数,它接收一个执行器函数,该执行器可以传递 resolve
和 reject
参数。
js
new Promise((resolve, reject) => {
// 待办任务:可能是某些异步操作
})
当调用 Promise
构造函数时,内部会发生某些事情:
- 创建一个
Promise
实例对象 ,该对象包含多个内部插槽,包括但不限于:[[PromiseState]]
[[PromiseResult]]
[[PromiseIsHandled]]
[[PromiseFulfillReactions]]
[[PromiseRejectReactions]]
- 创建一个
Promise
容器记录 ,这"封装"了该实例对象,并添加了某些额外功能,来完成或拒绝该实例。这些函数会控制Promise
的最终状态[[PromiseState]]
和结果[[PromiseResult]]
,并启动异步任务。
粉丝请注意,在 ES 语言说明书(ECMAScript Language Specification),[[]]
双重方括号表示外部用户不可见的内部插槽,大家可以简单理解为类似于 JS 中的类私有属性。
我们可以调用 resolve
来解析这个 Promise
,这可以通过执行器函数来做到。当我们调用 resolve
时:
[[PromiseState]]
会被设置为"fulfilled"
完成状态[[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
被拒绝时,该回调函数会被添加到微任务队列中。
到目前为止,我们只在执行器函数中直接调用了 resolve
或 reject
。尽管这是合法的,但它并没有充分榨干 Promise
的全部力量和主要目标!
大多数情况下,我们期望 resolve
或 reject
在稍后的某个时间点调用,通常是在异步任务完成时。
异步任务发生在主线程之外,比如读取文件 fs.readFile
、发出网络请求 https.get
或 XMLHttpRequest
,或简单的计时器 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
的处理程序使用。
最终,结果被记录。最后一个 then
的 Promise
的 [[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
的一个牛逼之处在于,如果通过 then
或 catch
附加处理程序,它可以触发异步操作。由于处理程序被推送到微任务队列,我们可以以非阻塞方式处理最终结果。这使得处理错误、将多个操作链接在一起变得轻而易举,并使代码更具可读性和可维护性!
Promise
仍然是一个基础概念,对于每个 JS 开发者而言都至关重要。
本期话题是 ------ 你最喜欢、或最不能接受 Promise
的哪些设计和行为?欢迎在本文下方自由言论,文明共享。
坚持阅读,自律打卡,每天一次,进步一点。
《前端暴走团》,喜欢请抱走!我是团长林语冰。谢谢大家的点赞,掰掰~