Promise A+规范翻译
Promise A+规范翻译
这是一个可靠的、可交互的JavaScript Promise的开放标准——由实现者负责,对实现者服务。
一个promise代表异步操作的最终结果,与promise交互的主要方式是通过它的.then方法,该方法注册回调以接收promise成功后的最终值或失败后的错误原因。
本规范详细说明了 then 方法的行为,为所有符合 Promises/A+ 规范的承诺实现提供了可互操作的基础。因此,该规范应被视为非常稳定。尽管 Promises/A+ 组织可能会偶尔对该规范进行小幅的向后兼容性更改,以解决新发现的特殊情况,但我们只会在经过仔细考虑、讨论和测试之后,才会将大型或不兼容的更改集成进来。
历史上,Promises/A+澄清了早期Promises/A提案的行为性条款,并将其扩展到涵盖实际行为,同时删除了定义不明确或存在问题的部分。
最后,核心的“Promises/A+”规范并没有涉及如何创建、实现或拒绝承诺的问题,而是选择专注于提供一个可互操作的“then”方法。在将来,配套规范的工作可能会涉及这些主题。
1、相关术语(Terminology)
1.1、“promise”是带有then方法的对象或函数,其行为符合此规范。
1.2、“thenable”是定义then方法的对象或函数
1.3、“value”是任何合法的JavaScript值(包括undefined、thenable或promise)
1.4、“exception”是使用throw语句抛出的值
1.5、“reason”是一个值,表示promise状态为rejected的原因
2、要求(Requirements)
2.1、Promise状态(Promise States)
一个promise必须处于以下三种状态之一:pending, fulfilled, 或者rejected
2.1.1、当promise处于pending状态时:
2.1.1.1、可以过渡到fulfilled状态或rejected状态
2.1.2、当promise处于fulfilled状态时:
2.1.2.1、不能转换到其他任何状态
2.1.2.2、必须有一个不可变值(value)
2.1.3、当promise处于rejected状态时:
2.1.3.1、不能转换到其他任何状态
2.1.3.2、必须有一个不可变原因(reason)
这里的“不得更改”意味着不可变的身份(即===),但并不意味着深度不可变性。
释:在 JavaScript 中,当我们谈论对象的身份(identify)时,我们实际上是在谈论对象的引用。每个对象在创建时都会分配一个唯一的内存地址,这个地址就是该对象的身份。当你把一个对象赋值给变量或传递给函数时,实际上是把这个对象的引用复制给了变量或函数参数。因此,如果你有两个变量指向同一个对象,那么这两个变量实际上共享了同一份数据,对其中一个变量所做的修改会影响到另一个变量。
Promise/A+ 规范中的 "must not change" 指的是一个对象的身份(即其引用)在特定情况下必须保持不变。这里的身份不变是通过 JavaScript 的严格相等运算符 (===
) 来判断的,意味着两个比较的对象必须是同一个对象实例。
具体来说,在 Promise/A+ 规范中,当提到值“必须不改变”时,是指一旦一个 Promise 被解析(fulfilled 或 rejected),它所关联的值的身份就不能再改变了。例如,如果一个 Promise 解析为一个特定的对象或数组,那么这个 Promise 之后总是会与那个特定的对象或数组相关联,你不能让这个 Promise 指向另一个新的对象或数组。
但是,“必须不改变”并不意味着该对象或数组的内容(属性或元素)是不可变的(deep immutability)。也就是说,虽然 Promise 的解析值不能被替换为不同的对象,但如果解析值本身是一个可变对象(如普通对象或数组),那么这个对象内部的属性还是可以被修改的。
示例如下:
let promise = Promise.resolve({ value: 42 }); // 尝试改变解析值为新的对象会违反规范 // 下面的行为是不被允许的: // promise = Promise.resolve({ value: 100 }); // 这不是改变了解析值,而是创建了一个新的 Promise
let obj = { value: 42 }; let promise = Promise.resolve(obj); // 修改 obj 的内容是允许的,因为它不影响对象的身份 obj.value = 100; promise.then(resolvedValue => { console.log(resolvedValue.value); // 输出 100, 因为 obj 的 .value 属性已经被改变。 });
2.2、关于 then 方法
Promise必须提供一个then方法来获取其当前或最终的value或reason。
Promise的then方法接受两个参数:
promise.then(onFulfilled, onRejected) {}
2.2.1、onFulfilled和onRejected都是可选参数:
2.2.1.1、如果onFulfilled不是一个函数,它必须被忽略。
2.2.1.1、如果onRejected不是一个函数,它必须被忽略。
2.2.2、如果onFulfilled是一个函数:
2.2.2.1、它必须在promise完成后(fulfilled)调用,并将promise的值(value)作为它的第一个参数。
2.2.2.2、禁止在promise完成前调用
2.2.2.3、禁止多次调用
2.2.3、如果onRejected是一个函数:
2.2.2.1、它必须在promise失败后(rejected)调用,并将promise的原因(reason)作为它的第一个参数。
2.2.2.2、禁止在promise失败前调用
2.2.2.3、禁止多次调用
2.2.4、onFulfilled 或 onRejected 必须在执行上下文栈仅包含平台代码时才被调用。[3.1]
释:这句话意味着,只有当所有的用户代码都已经执行完毕,JavaScript 引擎正在处理平台代码(例如浏览器或 Node.js 的内置代码)时,onFulfilled
或 onRejected
回调函数才会被调用。这确保了这些回调函数总是异步执行,即使关联的 Promise 立即被解析。
Promise/A+ 规范的第 3.1 条要求确保 onFulfilled
和 onRejected
回调函数不会立即执行,而是会在当前执行栈清空之后再执行。这意味着回调函数必须在当前同步代码完全执行完毕,并且 JavaScript 引擎回到事件循环(event loop)处理任务队列中的下一个任务时才会被调用。
执行上下文和调用栈:
JavaScript 是单线程语言,这意味着它一次只能做一件事。当代码运行时,会形成一个“执行上下文栈”,其中每个函数调用都会创建一个新的执行上下文并将其推入栈中。当函数完成执行时,对应的执行上下文会被从栈中弹出。
平台代码 (Platform Code):
平台代码指的是由 JavaScript 运行环境提供的代码,例如浏览器或 Node.js 中的内置函数。这些代码通常用于处理异步操作,如定时器、网络请求、DOM 操作等。
根据 Promise/A+ 规范的第 3.1 条:
onFulfilled
或onRejected
必须在当前执行上下文栈只包含平台代码时才被调用。- 这意味着只要有一个同步代码块正在执行,那么
onFulfilled
或onRejected
不应该在这个同步代码块结束之前被执行。 - 这个规则确保了所有的
then
回调都是异步执行的,即使 Promise 立即被解析(fulfilled 或 rejected)。
示例如下:
let promise = Promise.resolve('foo'); promise.then(function(value) { console.log(value); }); console.log('bar'); // bar // foo
这种设计有以下几个好处:
- 它提供了一致的行为:无论 Promise 是立即解析还是在未来的某个时间点解析,
then
回调总是异步执行。 - 它避免了潜在的竞态条件(race conditions),因为所有依赖于 Promise 解析状态的操作都会以一致的方式排队等待执行。
- 它使得开发者可以更容易地预测代码的执行顺序,从而编写更可靠和可维护的代码。
2.2.5、onFulfilled和onRejected必须作为函数调用(即使不存在this值)
释:根据 Promise/A+ 规范的第 3.2 条,onFulfilled
和 onRejected
必须作为函数被调用(即不带有任何特定的 this
值)。这意味着当这些回调函数被调用时,它们不会绑定到任何对象上;换句话说,它们将以非方法的形式被调用,因此在回调函数内部,this
将不会指向定义回调时所在的对象。
Promise/A+ 规范要求 onFulfilled
和 onRejected
回调必须以函数形式调用,而不是作为对象的方法调用。这确保了回调函数的执行环境是独立的,不会受到外部对象状态的影响。也就是说,在回调函数中,this
将是 undefined
(如果代码运行在严格模式下),或者它将是全局对象(如果代码运行在非严格模式下)。
无论这些回调是在哪个对象的上下文中定义的,当它们被调用时,this
都不会指向那个对象。这为开发者提供了一个一致且可预测的行为,避免了由于意外的 this
绑定导致的错误。
示例如下:
let promise = new Promise((resolve, reject) => { resolve('success'); }); promise.then(function onFulfilled(value) { console.log(this); // 在严格模式下输出: undefined // 在非严格模式下输出: Window (在浏览器环境中) console.log(value); // 输出: success }, function onRejected(error) { console.log(this); // 在严格模式下输出: undefined // 在非严格模式下输出: Window (在浏览器环境中) console.log(error); });
2.2.6、then方法可以在同一个promise上多次调用
2.2.6.1、如果/当promise的状态是fulfilled时,onFulfilled的回调函数必须按照其原始顺序执行
2.2.6.2、如果/当promise的状态是onRejected时,onRejected的回调函数必须按照其原始顺序执行
释:这一要求确保了回调函数的执行顺序是可预测的,并且与它们注册的顺序一致。无论 Promise 是立即被解析还是在未来的某个时间点被解析,onFulfilled
回调的执行顺序都保持一致。这为开发者提供了一个可靠的行为预期,使得异步代码更容易理解和调试。
2.2.7、then方法必须返回一个promise
promise2 = promise1.then(onFulfilled, onRejected);
2.2.7.1、如果 onFulfilled
或 onRejected
回调函数返回了一个值 x
,则必须运行 Promise 解析程序 (Promise Resolution Procedure),即调用 [[Resolve]](promise2, x)
,这里的 promise2
是由 then
方法返回的新 Promise。
释:这一条规范确保了当 onFulfilled
或 onRejected
回调函数返回一个值时,这个值会被正确地解析并影响到由 then
返回的新 Promise 的状态。具体来说,[[Resolve]](promise2, x)
是一个内部操作,用于决定新 Promise (promise2
) 的最终状态。它会检查 x
是否为一个 Promise 或者是一个普通值,并据此来决定 promise2
应该被解析为成功还是失败。而根据其返回值,将会出现以下情况:
- 如果
x
是一个普通值(例如数字、字符串、对象等),那么promise2
将被解析为成功,并且其值为x
。 - 如果
x
是一个已经解析的 Promise,那么promise2
将采用x
的最终状态和值。 - 如果
x
是一个未解析的 Promise,那么promise2
将与x
链接起来,也就是说,promise2
的最终状态将取决于x
的最终状态。 - 如果
x
是一个抛出错误的结果,则promise2
将被拒绝(rejected)并且错误将成为promise2
的原因。
示例如下:
let promise1 = Promise.resolve('success'); // onFulfilled 返回一个普通值 promise1.then(function onFulfilled(value) { console.log('First callback:', value); // 输出: First callback: success return 'new value'; }).then(function onFulfilled(value) { console.log('Second callback:', value); // 输出: Second callback: new value }); // onFulfilled 返回一个新的 Promise promise1.then(function onFulfilled(value) { console.log('Third callback:', value); // 输出: Third callback: success return Promise.resolve('another value'); }).then(function onFulfilled(value) { console.log('Fourth callback:', value); // 输出: Fourth callback: another value }); // onFulfilled 抛出错误 promise1.then(function onFulfilled(value) { console.log('Fifth callback:', value); // 输出: Fifth callback: success throw new Error('an error occurred'); }).catch(function onError(error) { console.error('Error caught:', error.message); // 输出: Error caught: an error occurred });
2.2.7.2、如果 onFulfilled
或 onRejected
回调函数抛出了一个异常 e
,那么由 then
方法返回的新 Promise (promise2
) 必须被拒绝(rejected),并且以抛出的异常 e
作为拒绝的原因。
释:这条规则确保了任何在回调函数中发生的错误都能够被正确地传播,并且可以通过链式调用中的后续 catch
方法或其他 onRejected
回调来处理。
当 onFulfilled
或 onRejected
抛出异常时,规范要求如下:
-
- 立即拒绝:
promise2
应该立即进入拒绝状态,并将抛出的异常e
作为其拒绝的原因。 - 异常传播:这意味着异常不会被静默吞掉,而是会被传递给链中的下一个错误处理器,使得开发者可以捕获并处理这些异常。
- 防止未捕获的异常:通过这种方式,Promise 链能够避免未捕获的异常导致程序崩溃的问题,提供了更安全的异步编程模型。
- 立即拒绝:
2.7.7.3、如果 onFulfilled
不是一个函数,并且 promise1
被解析(fulfilled),那么由 then
方法返回的新 Promise (promise2
) 必须被解析为与 promise1
相同的值
释:这意味着 promise2
将直接采用 promise1
的成功值,而不会尝试执行任何回调逻辑。这条规范确保了即使开发者没有提供有效的 onFulfilled
回调函数,Promise 链也不会中断或出现意外行为。如果 onFulfilled
不是函数,promise2
仍然会被解析,并且其值与 promise1
的值相同。这保证了值可以在 Promise 链中正确地传递下去。这种处理方式简化了错误处理逻辑,因为不需要担心由于提供了非函数类型的 onFulfilled
参数而导致的错误。
示例如下:
let promise1 = Promise.resolve('success value'); // onFulfilled 不是函数 promise1.then(null, function onRejected(error) { // 这个回调不会被执行,因为 promise1 是 fulfilled 状态 }).then(function onFulfilled(value) { console.log('Second then:', value); // 输出: Second then: success value }); // 另一个例子,onFulfilled 是 undefined promise1.then(undefined).then(function onFulfilled(value) { console.log('Third then:', value); // 输出: Third then: success value });
2.2.7.4、如果 onRejected
不是一个函数,并且 promise1
被拒绝(rejected),那么由 then
方法返回的新 Promise (promise2
) 必须被拒绝,并且以与 promise1
相同的原因作为其拒绝的理由。
2.3、Promise 解析过程(Promise Resolution Procedure)
Promise 解析过程是一个抽象操作,它接收一个 Promise 和一个值作为输入,我们将其表示为 [[Resolve]](promise, x)
。如果 x
是一个 thenable(即具有 then
方法的对象),则尝试使 promise
采用 x
的状态,前提是 x
至少在某种程度上表现得像一个 Promise。否则,将以值 x
来解析(fulfill)promise
。
这种对 thenable 对象的处理方式允许不同的 Promise 实现之间进行互操作,只要它们提供了符合 Promises/A+ 规范的 then
方法。它还允许 Promises/A+ 实现“同化”那些具有合理 then
方法的非标准 Promise 实现。
释:[[Resolve]]
是一个理论上存在的操作,用来描述如何处理 Promise 和传入的值之间的关系。如果传入的值 x
是一个 thenable(例如,任何实现了 then
方法的对象),解析过程会尝试让原始的 promise
采用 x
的状态,这使得不同实现的 Promise 可以相互协作。如果 x
不是 thenable,则直接用 x
的值来解析 promise
。通过这种方式,即使 Promise 实现不完全遵循 Promises/A+ 规范,只要它们有合理的 then
方法,就可以与标准的 Promise 实现一起工作,从而增强了不同库或平台上的 Promise 实现之间的兼容性和互操作性。
想要执行[[Resolve]](promise, x),请执行以下步骤:
2.3.1、如果 promise
和 x
引用的是同一个对象,则应以 TypeError
作为原因拒绝(reject)promise
。
释:这一规则是为了防止循环引用问题,确保 Promise 的解析过程不会陷入无限递归的状态。它规定了当 [[Resolve]](promise, x)
操作中的 x
与 promise
是同一个对象时的行为。这种情况下,直接拒绝 promise
并提供一个 TypeError
作为拒绝的原因,可以避免潜在的逻辑错误和性能问题。
示例如下:
let p = new Promise((resolve, reject) => { resolve(p); // 尝试将 promise 解析为自身 }); p.catch(error => { console.error('Promise rejected with reason:', error); // 输出: Promise rejected with reason: TypeError: Chaining cycle detected for promise #<Promise> });
2.3.2、如果 x
是一个 Promise,则应采用其状态:
2.3.2.1、如果x
是pending,那么 promise
必须保持pending状态,直到 x
被解析(fulfilled)或拒绝(rejected)。
释:这确保了当一个 Promise 依赖于另一个 Promise 的状态时,它不会提前改变自己的状态。如果 x
是一个尚未完成的 Promise(即处于 pending 状态),那么 promise
不会立即被解析或拒绝,而是等待 x
的最终状态。一旦 x
的状态发生变化(无论是被解析还是被拒绝),promise
将相应地更新其状态,以反映 x
的最终结果。
示例如下:
let x = new Promise((resolve, reject) => { setTimeout(() => resolve('success'), 2000); // 模拟异步操作,在2秒后解析 }); let promise = new Promise((resolve, reject) => { // 将 x 的状态应用到 promise 上 [[Resolve]](promise, x); }); // 监听 promise 的状态变化 promise.then(value => { console.log('Promise fulfilled with:', value); }).catch(error => { console.error('Promise rejected with:', error); }); console.log('Initial state of promise:', promise); // 输出: // Initial state of promise: [object Promise] (此时 promise 还处于 pending 状态) // (大约2秒后) // Promise fulfilled with: success
2.3.2.2、当 x
被解析(fulfilled)时,promise
也应被解析,并使用与 x
相同的值。
2.3.2.3、当 x
被拒绝(rejected)时,promise
也应被拒绝,并使用与 x
相同的原因。
释:这两条规则确保了一个 Promise 可以安全地“继承”另一个 Promise 的解析状态和结果,这对于构建复杂的异步流程控制逻辑非常重要。通过这种方式,开发者可以轻松地将多个异步操作链接在一起,并确保每个操作都能正确处理前一个操作的结果。此外,这还保证了 Promise 链的行为可预测性和一致性,增强了程序的稳定性和可靠性。
在实际开发中,这种行为非常有用,特别是在处理一系列依赖性异步操作时。例如,你可能有一个获取用户数据的 Promise,然后基于这些数据发起另一个请求。你可以使用这个规则来确保第二个请求的 Promise 只有在第一个请求成功完成后才会开始,并且它将接收第一个请求的结果作为输入。
2.3.3、否则,如果 x
是一个对象或函数,
2.3.3.1、让 then
成为 x.then
2.3.3.2、如果在尝试获取 x.then
属性时抛出了异常 e
,那么应当以 e
作为拒绝的理由来拒绝 promise
2.3.3.3、如果 then
是一个函数,则应使用 x
作为 this
值调用它,并传递两个参数:resolvePromise
和 rejectPromise
。这两个参数是回调函数,用于解析或拒绝 promise
。具体来说:
2.3.3.3.1、当 resolvePromise
被调用并传递一个值 y
时,应运行 [[Resolve]](promise, y)
2.3.3.3.2、当 rejectPromise
被调用并传递一个拒绝理由 r
时,应立即以该理由 r
拒绝 promise
。
2.3.3.3.3、如果 resolvePromise
和 rejectPromise
都被调用了,或者对同一个回调函数(即 resolvePromise
或 rejectPromise
)进行了多次调用,则只有第一次调用是有效的,后续的调用将被忽略。
2.3.3.3.4、如果在调用 then
方法时抛出了异常 e
:
2.3.3.3.4.1、若 resolvePromise
或 rejectPromise
已经被调用,则后续对它们的任何调用都应被忽略
2.3.3.3.4.2、否则,应该以e
为reason拒绝promise
释:
-
-
初始化标志变量:
- 使用一个标志变量(例如
called
)来跟踪是否已经调用了resolvePromise
或rejectPromise
,以确保每个回调最多只响应一次。
- 使用一个标志变量(例如
-
尝试调用
then
方法:- 使用
x
作为this
值调用then
方法,并传递resolvePromise
和rejectPromise
作为参数。 - 如果在这个过程中抛出了异常
e
,并且此时还没有调用resolvePromise
或rejectPromise
,则应立即拒绝promise
并将e
作为拒绝的理由。
- 使用
-
处理
resolvePromise
和rejectPromise
的调用:- 在每次调用
resolvePromise
或rejectPromise
时,首先检查called
标志。 - 如果
called
已经为true
,则直接返回,不执行任何操作。 - 如果
called
为false
,则设置called = true
并继续执行相应的解析或拒绝逻辑。- 对于
resolvePromise
:如果被调用并传递了值y
,则运行[[Resolve]](promise, y)
。 - 对于
rejectPromise
:如果被调用并传递了拒绝理由r
,则立即将promise
拒绝,并将提供的原因r
作为拒绝的理由。
- 对于
- 在每次调用
-
确保只有一个回调被调用:
- 这种机制确保即使
then
方法内部尝试多次调用这些回调,也只响应第一次调用,从而避免了状态冲突。
- 这种机制确保即使
-
处理异常:
- 在调用
then
方法的try...catch
块中捕获任何抛出的异常。 - 如果捕获到异常
e
,并且called
标志仍然为false
,则调用reject(e)
来拒绝promise
。
- 在调用
-
示例如下:
function resolvePromise(promise, x, resolve, reject) { // 检查循环引用 if (promise === x) { return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } let then; let called = false; try { // 尝试安全地获取 x.then then = x.then; } catch (error) { // 如果获取 x.then 抛出异常,且尚未调用 resolve/reject,则拒绝 promise if (!called) reject(error); return; } // 检查 then 是否为函数 if (typeof then === 'function') { try { // 调用 then 方法,传递 resolvePromise 和 rejectPromise 回调 then.call(x, y => { if (called) return; called = true; [[Resolve]](promise, y, resolve, reject); // 递归调用以处理可能的嵌套 thenable }, r => { if (called) return; called = true; reject(r); // 拒绝 promise 并提供拒绝理由 r } ); } catch (error) { // 如果调用 then 抛出异常且尚未调用 resolve/reject,则拒绝 promise if (!called) reject(error); } } else { // 如果 then 不是函数,则直接解析 promise if (!called) resolve(x); } } // [[Resolve]] 函数模拟 function [[Resolve]](promise, y, resolve, reject) { if (promise === y) { return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } let then; let called = false; try { then = y.then; } catch (error) { if (!called) reject(error); return; } if (typeof then === 'function') { try { then.call(y, z => { if (called) return; called = true; [[Resolve]](promise, z, resolve, reject); // 递归解析 }, e => { if (called) return; called = true; reject(e); } ); } catch (error) { if (!called) reject(error); } } else { if (!called) resolve(y); } } // 使用示例:一个会抛出异常的 thenable 对象 let faultyThenable = { then: function(resolve, reject) { throw new Error('Error in then method'); } }; let promise = new Promise((resolve, reject) => { resolvePromise(promise, faultyThenable, resolve, reject); }); promise.then(value => { console.log('Promise fulfilled with:', value); }).catch(error => { console.error('Promise rejected with:', error.message); }); console.log('Initial state of promise:', promise); // 输出: // Initial state of promise: [object Promise] (此时 promise 还处于 pending 状态) // (几乎立即) // Promise rejected with: Error in then method
通过这种方式,确保了即使在处理 then
方法调用时发生了意外错误,这些错误也能被正确地传播和处理。这增强了 Promise 链的健壮性,使得开发者可以更加自信地编写复杂的异步逻辑,而不必担心由于内部错误导致的状态不一致或未处理的异常。通过严格的回调管理和异常处理,保证了 Promise 链的行为可预测性和一致性,增强了程序的稳定性和可靠性。
强调点
- 唯一性保证:一旦
resolvePromise
或rejectPromise
被调用,后续的任何调用都会被忽略。 - 立即拒绝:一旦检测到异常
e
,并且resolvePromise
或rejectPromise
尚未被调用,则立即将promise
拒绝,并将e
作为拒绝的原因。 - 异常处理:在处理
then
方法调用的过程中,任何抛出的异常也应当作为拒绝的理由来拒绝promise
,但前提是called
标志仍然为false
。
2.3.3.4、如果 then
不是函数,应直接以 x
作为值来解析(fulfill)promise
释:如果尝试获取 x.then
并发现它不是一个函数,则 x
被视为一个普通值而非 thenable 对象。在这种情况下,promise
应该立即被解析,并将 x
作为其值。一旦检测到异常 e
,并且 resolvePromise
或 rejectPromise
尚未被调用,则立即将 promise
拒绝,并将 e
作为拒绝的原因。
2.3.4、如果 x
不是一个对象或函数(即 x
是一个原始值,如数字、字符串、布尔值、null
或 undefined
),则应该直接以 x
作为值来解析(fulfill)promise
释:如果 x
是一个原始值,promise
应该立即被解析,并将 x
作为其值。这避免了不必要的处理步骤,因为原始值不能具有 then
方法,因此不可能是 thenable 对象。
如果一个 Promise 被解析为一个参与了循环引用链的 thenable 对象,使得 [[Resolve]](promise, thenable)
的递归调用最终再次触发 [[Resolve]](promise, thenable)
,那么遵循上述算法将导致无限递归。我们鼓励实现(但不是必需的)检测这种递归,并以提供信息的TypeError作为理由拒绝promise。
释:当一个 Promise 被解析为一个 thenable 对象,而这个 thenable 对象又直接或间接地引用了最初的 Promise 或者其他已经出现在链中的 Promise,这就形成了一个循环。在处理这种循环时,如果不加以控制,[[Resolve]]
函数会不断地尝试解析同一个 Promise 或 thenable 对象,从而导致无限递归。
为了实现检测,可以在 [[Resolve]]
函数中加入一个机制来跟踪已经访问过的 thenable 对象。如果发现即将解析的对象已经在之前的解析链中出现过,则可以认为发生了循环引用,并立即拒绝当前的 Promise。这种方法能有效防止无限递归,同时提供了一个清晰的错误报告给开发者。
3、注释(Notes)
3.1、这里的“平台代码”指的是 JavaScript 引擎、运行环境及 Promise 实现代码。实际上,这项规定确保了 onFulfilled
和 onRejected
回调函数将在调用 then
方法的事件循环轮次之后异步执行,并且在新的调用栈中执行。这种异步行为可以通过宏任务机制(例如 setTimeout
或 setImmediate
)或微任务机制(例如 MutationObserver
或 Node.js 的 process.nextTick
)来实现。由于 Promise 实现被视作平台代码,它内部可以包含一个任务调度队列或称为“蹦床”的机制,用于调度和执行这些回调函数。
释:
这段描述强调了几个关键点:
- 异步执行:
onFulfilled
和onRejected
必须在调用then
的那个事件循环轮次之后执行,以确保它们不会立即同步执行。 - 新调用栈:这些回调函数应该在一个全新的调用栈中执行,而不是在当前的调用栈中继续执行,从而避免阻塞其他操作。
- 任务调度机制:为了实现上述行为,可以使用宏任务或微任务机制。宏任务会等待当前所有代码执行完毕后才执行,而微任务则会在当前操作完成后、下一个宏任务之前执行。
- 蹦床机制:某些 Promise 实现可能会包含自己的任务调度系统,确保回调函数按照正确的顺序和时机执行。
通过这种方式,确保了 Promise 的回调函数能够在正确的时间点执行,同时不影响其他代码的正常运行,提供了更好的并发性和响应性。
什么是蹦床机制?
蹦床机制(Trampoline Mechanism)是一种编程技术,主要用于递归函数调用的优化和控制异步任务的调度。在 JavaScript 的上下文中,它通常用于确保回调函数按照正确的顺序执行,并且每个回调都在一个新的调用栈中执行,避免了同步执行导致的栈溢出问题。
蹦床机制的工作原理:
蹦床机制的核心思想是将递归或连续的任务分解成一系列独立的小任务,并通过某种方式(如事件循环、微任务队列等)依次调度这些任务。这样做可以:
- 避免栈溢出:对于深度递归调用,每次递归都会增加调用栈的深度。蹦床机制通过将递归转换为迭代并使用调度器来处理每个步骤,从而避免了栈溢出。
- 实现异步行为:可以确保每个任务在一个新的调用栈中执行,从而提供真正的异步行为,这对于 JavaScript 这样的单线程语言尤为重要。
- 简化代码逻辑:通过将复杂的递归逻辑转换为更简单的迭代逻辑,可以使代码更容易理解和维护。
在promise中如何应用?
在 Promise 实现中,蹦床机制通常用于确保 onFulfilled
和 onRejected
回调函数在合适的时机执行。具体来说:
-
- 任务调度:当一个 Promise 被解析或拒绝时,它的回调函数不会立即同步执行,而是被放入一个任务队列中等待调度。
- 异步执行:蹦床机制确保这些回调函数会在当前事件循环轮次之后,在一个新的调用栈中异步执行。
- 避免阻塞:这种方式避免了同步执行可能带来的阻塞问题,确保了其他代码可以继续运行而不被长时间占用。
具体实现:
function trampoline(fn) { let result = fn; while (typeof result === 'function') { result = result(); } return result; } // 模拟一个递归函数 function recursiveTask(n) { if (n <= 0) { return n; } else { return () => recursiveTask(n - 1); } } // 使用蹦床机制避免栈溢出 const result = trampoline(recursiveTask(1000)); console.log(result); // 输出: 0 // 在 Promise 实现中的应用 class MyPromise { constructor(executor) { this.callbacks = []; executor(this.resolve.bind(this), this.reject.bind(this)); } resolve(value) { setTimeout(() => { // 使用宏任务或微任务机制 this.callbacks.forEach(callback => { callback(value); }); }, 0); } reject(reason) { setTimeout(() => { // 使用宏任务或微任务机制 this.callbacks.forEach(callback => { callback(null, reason); }); }, 0); } then(onFulfilled, onRejected) { this.callbacks.push((value, reason) => { if (value !== undefined) { onFulfilled(value); } else if (reason !== undefined) { onRejected(reason); } }); return this; } } // 使用 MyPromise new MyPromise((resolve, reject) => { resolve('Success!'); }).then( value => console.log('Fulfilled:', value), reason => console.error('Rejected:', reason) );
在这个例子中:
trampoline
函数用于递归调用recursiveTask
,但通过将每次递归转换为一个新的函数调用来避免栈溢出。MyPromise
类模拟了一个简单的 Promise 实现,其中resolve
和reject
方法使用setTimeout
来确保回调函数在下一个事件循环轮次中异步执行。这类似于蹦床机制的作用,确保每个回调函数在一个新的调用栈中执行。
通过将递归或连续的任务分解为一系列独立的小任务,并通过调度器依次执行这些任务,避免了栈溢出问题,同时提供了更好的异步行为。在 Promise 实现中,蹦床机制确保了回调函数能够在正确的时间点执行,同时不影响其他代码的正常运行,提供了更好的并发性和响应性。
3.2、也就是说,在严格模式(strict mode)下,this
在这些函数内部将是 undefined
;而在非严格模式(sloppy mode)下,this
将是全局对象(global object)。
3.3、实现可以允许 promise2
和 promise1
是同一个对象(即 promise2 === promise1
),但前提是该实现必须满足所有规范要求。每个实现应当文档化其是否可以在特定条件下生成 promise2 === promise1
的情况,并明确描述这些条件。
释:这段话主要讨论了 JavaScript Promise 实现中的一个特定行为:在调用 Promise.prototype.then()
方法时,新返回的 Promise (promise2
) 是否可以与原始的 Promise (promise1
) 是同一个对象实例。根据 Promises/A+ 规范,虽然这不是强制要求的行为,但实现可以选择这样做,只要它不违反其他规范要求。
允许 promise2 === promise1
可以带来一些性能优化,因为不需要创建新的 Promise 对象。然而,这也可能使代码的行为更难以预测,特别是在依赖于不同 Promise 实例的地方。因此,实现者需要权衡利弊,并确保这种行为不会导致意外问题。
具体实现:
// 简单的 Promise 实现,允许 promise2 === promise1 class MyPromise { constructor(executor) { this.callbacks = []; executor(this.resolve.bind(this), this.reject.bind(this)); } resolve(value) { // 处理解析逻辑 } reject(reason) { // 处理拒绝逻辑 } then(onFulfilled, onRejected) { // 在某些条件下返回相同的 Promise 实例 if (/* 某些条件 */) { return this; } const newPromise = new MyPromise((resolve, reject) => { // 添加回调到当前 Promise 的队列中 this.callbacks.push({ onFulfilled, onRejected, resolve, reject }); }); return newPromise; } } // 使用 MyPromise const promise1 = new MyPromise((resolve, reject) => { setTimeout(() => resolve('Success!'), 1000); }); const promise2 = promise1.then(value => console.log(value)); // 根据实现的具体条件,这里可能会输出 true 或 false console.log(promise2 === promise1); // 输出: true 或 false
3.4、通常情况下,只有当 x
来自当前的 Promise 实现时,我们才能确定 x
是一个真正的 Promise。此条款允许实现使用特定的方法来处理来自已知符合 Promises/A+ 规范的其他实现的 Promise,从而采用它们的状态。
释:
这段描述主要讨论了在 Promise 实现中如何处理外部来源的 Promise(即不是由当前实现创建的 Promise)。具体来说:
-
确认
x
是否为真正 Promise:由于不同库或环境中的 Promise 实现可能有所不同,通常只有当x
是由当前实现创建的 Promise 时,才能确保它是一个真正的、符合规范的 Promise。 -
特定于实现的方法:为了兼容和高效地处理来自其他符合规范的 Promise 实现的对象,允许当前实现使用特定于自己的方法来“采纳”这些外部 Promise 的状态。这意味着当前实现可以优化对这些外部 Promise 的处理,而不必每次都通过通用的
then
方法进行解析。 -
已知符合规范的 Promise:如果某个外部 Promise 来自一个已知符合 Promises/A+ 规范的实现,那么当前实现可以安全地假设该 Promise 行为正确,并使用更高效的内部机制来处理它。
允许这种行为的主要原因是提高性能和效率。对于已知符合规范的 Promise,当前实现可以通过内部优化来直接处理其状态,而不是每次都通过标准的 then
方法进行间接处理。这不仅可以减少不必要的函数调用和对象创建,还可以提供更一致的行为。
示例如下:
class MyPromise { constructor(executor) { this.state = 'pending'; this.value = undefined; this.callbacks = []; executor(this.resolve.bind(this), this.reject.bind(this)); } resolve(value) { if (this.state === 'pending') { this.state = 'fulfilled'; this.value = value; this.executeCallbacks(); } } reject(reason) { if (this.state === 'pending') { this.state = 'rejected'; this.value = reason; this.executeCallbacks(); } } then(onFulfilled, onRejected) { const newPromise = new MyPromise((resolve, reject) => { this.callbacks.push({ onFulfilled, onRejected, resolve, reject }); }); return newPromise; } executeCallbacks() { // 执行所有回调 } static resolve(x) { // 如果 x 是当前实现的 MyPromise 实例,则直接返回 x if (x instanceof MyPromise) { return x; } // 如果 x 是已知符合规范的外部 Promise,可以直接采用其状态 if (isKnownConformantPromise(x)) { return new MyPromise((resolve, reject) => { x.then(resolve, reject); }); } // 否则,按照常规流程解析 x return new MyPromise(resolve => resolve(x)); } } function isKnownConformantPromise(x) { // 假设这是一个检查 x 是否来自已知符合规范的 Promise 实现的函数 // 这里只是一个示例,实际应用中需要具体的逻辑来判断 return typeof x === 'object' && typeof x.then === 'function'; } // 使用 MyPromise const externalPromise = Promise.resolve('External Value'); const myPromise = MyPromise.resolve(externalPromise); myPromise.then(value => console.log('Resolved with:', value)); // 输出: // Resolved with: External Value
3.5、通过首先存储 x.then
的引用,然后测试并调用这个引用,这种方法避免了多次访问 x.then
属性。这种预防措施对于确保一致性至关重要,尤其是在处理访问器属性时,因为访问器属性的值可能在不同时间点的访问之间发生变化。
释:
这段描述强调了一种处理 Promise 和 thenable 对象时的最佳实践,特别是在 JavaScript 中,对象属性可以是数据属性或访问器属性(getter/setter)。访问器属性的值可能会在每次访问时动态计算,因此其值在不同的访问之间可能是不一致的。关键点在于:
-
避免多次访问:通过将
x.then
的值存储在一个变量中,可以确保只访问一次x.then
属性。这不仅提高了性能,还避免了因属性值变化而引起的潜在问题。 -
确保一致性:如果
x.then
是一个访问器属性,它的值可能会在每次访问时发生变化。通过一次性获取并存储x.then
的引用,可以确保后续的操作基于同一个值进行,从而保持行为的一致性。 -
最佳实践:这种做法是一种常见的编程技巧,用于确保代码在面对动态或不确定的属性值时的行为更加稳定和可预测。
示例如下:
function resolvePromise(promise, x, resolve, reject) { let called = false; // 首先存储对 x.then 的引用 let then; try { then = x.then; } catch (error) { return reject(error); } // 然后测试该引用是否为函数 if (typeof then === 'function') { try { // 最后调用该引用 then.call(x, y => { if (called) return; called = true; resolvePromise(promise, y, resolve, reject); }, r => { if (called) return; called = true; reject(r); } ); } catch (error) { if (!called) reject(error); } } else { // 如果 then 不是函数,则直接解析 promise if (!called) resolve(x); } } // 使用示例 const thenable = { get then() { console.log('Accessing then...'); return function(onFulfilled, onRejected) { onFulfilled('Resolved!'); }; } }; const promise = new Promise((resolve, reject) => { resolvePromise(promise, thenable, resolve, reject); }); promise.then(value => console.log('Promise fulfilled with:', value)); // 输出: // Accessing then... // Promise fulfilled with: Resolved!
3.6、实现不应该对 thenable 链的深度施加任意限制,并假定超出该限制后的递归会无限进行。只有在检测到真正的循环引用时,才应该抛出 TypeError
;对于由不同 thenable 组成的无限链,正确的处理方式是继续递归,而不是提前终止。
释:
这段描述强调了在处理 Promise 和 thenable 对象时,实现应如何正确地处理潜在的无限递归问题。具体来说:
-
不设限:实现不应为 thenable 链的深度设置固定的上限。这样做可能会错误地将合法的长链误判为无限递归,从而过早地终止处理。
-
仅处理真正循环:只有当检测到实际的循环引用(例如,一个 thenable 间接或直接指向自己)时,才应该抛出
TypeError
并拒绝 Promise。这是为了避免陷入无限递归,因为真正的循环确实会导致这种情况。 -
正确处理无限链:对于由不同 thenable 组成的无限链,即使链条非常长,也应当继续递归处理,直到最终解析或拒绝。这种情况下,递归不会进入无限循环,因为每个 thenable 都是不同的对象。
关键点
- 避免过早终止:通过不设置任意的深度限制,可以确保合法的长链不会被错误地终止。
- 准确识别循环引用:只有在真正检测到循环引用时才抛出错误,这有助于保持行为的一致性和可靠性。
- 支持无限链:对于由不同 thenable 组成的无限链,实现应当能够继续处理,而不应因长度而提前终止。
示例如下:
function [[Resolve]](promise, x, resolve, reject, seen = new Set()) { // 检查是否已经访问过 x,以检测循环引用 if (seen.has(x)) { return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); } seen.add(x); let called = false; // 如果 x 不是对象或函数,则直接解析 promise if (x !== null && typeof x !== 'object' && typeof x !== 'function') { if (!called) resolve(x); return; } try { // 尝试安全地获取 x.then let then = x.then; // 如果 then 不是函数,则直接解析 promise if (typeof then !== 'function') { if (!called) resolve(x); return; } // 调用 then 方法,传递 resolvePromise 和 rejectPromise 回调 try { then.call(x, y => { if (called) return; called = true; [[Resolve]](promise, y, resolve, reject, seen); // 递归调用 }, r => { if (called) return; called = true; reject(r); // 拒绝 promise 并提供拒绝理由 r } ); } catch (error) { // 如果调用 then 抛出异常且尚未调用 resolve/reject,则拒绝 promise if (!called) reject(error); } } catch (error) { // 如果获取 x.then 抛出异常,且尚未调用 resolve/reject,则拒绝 promise if (!called) reject(error); } } // 使用示例:模拟无限链 function createThenable(value) { return { then: function(onFulfilled, onRejected) { const nextThenable = createThenable(value + 1); onFulfilled(nextThenable); return nextThenable; } }; } const infiniteChain = createThenable(0); const promise = new Promise((resolve, reject) => { [[Resolve]](promise, infiniteChain, resolve, reject); }); // 注意:由于这是一个无限链,理论上它会一直递归下去,但浏览器或 Node.js 可能会在某个点抛出栈溢出错误。 // 实际应用中,这种无限链通常是不推荐的,因为它可能导致性能问题或崩溃。
此文为Promise A+规范翻译及各类讲解资料整理
本文作者:草丛莽撞人
本文链接:https://www.cnblogs.com/tagzeee/p/18603188
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步