
WebKit 修复 Promise 未决 bug,Bun 与 Safari 不再挂起
来源标题:Fixing a WebKit bug to see if promises are made of train tracks
WebKit 修复了一个导致 Promise 永久挂起、程序无法结束的 bug,修复已提交至 WebKit 仓库,仅增加 16 行代码。该问题表现为:当在 await 时读取 Promise 的 constructor 属性触发异常,Bun 1.3.14 和 Safari 26 会打印错误后一直挂起,而 Node.js 则正常崩溃退出。由于 Bun 和 Safari 均使用 JavaScriptCore 引擎,两者行为一致,该问题对应 WebKit bug 318399。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
这是一个直到今年七月在 Bun 或 Safari 中都无法运行完毕的程序。
const p = Promise.resolve(1); p.constructor = { get Symbol.species { throw new Error("boom"); }; console.log("before"); await p; console.log("done");
看不懂也没关系。JavaScript 规范规定,由于那个 getter 抛出了错误,所以 await p 也应该抛出错误,程序应该崩溃。Node.js 确实会崩溃,但 Safari 26 和 Bun 1.3.14 会打印出 "before",报告错误,然后挂起。
$ node never.mjs before Error: boom exit 1 $ bun 1.3.14 $ bun never.mjs before error: boom
Safari 26 的失败方式与 Bun 相同:
const p = Promise.resolve(1); p.constructor = { get Symbol.species { throw new Error("boom"); }; console.log("before"); await p; console.log("done"); before Error: boom
Bun 和 Safari 都内嵌了 JavaScriptCore,所以行为是一样的——这个 promise 永远不会被 settle!它既没有被 fulfil 也没有被 reject,所以永远处于 pending 状态。这就是 WebKit bug 318399。我在 WebKit/WebKit#68749 中修复了它,修复只增加了 16 行代码!
当你在 JavaScript 中构造一个新的 promise 时,引擎会创建两个函数 resolve 和 reject,并将它们传递给你传给 promise 构造函数的回调。
new Promise((resolve, reject) => { console.log("I got my resolve function!", resolve); console.log("I got my reject function!", reject); resolve(42); }); // In V8 this logs: // I got my resolve function! ƒ () { [native code] } // I got my reject function! ƒ () { [native code] }
内置辅助函数 Promise.resolve(value) 做的事情是一样的:构造一个 promise 并立即用你传入的值调用 resolve(例外情况是,如果 value 是一个 promise 且它的 .constructor 属性等于 Promise,则原样返回)。一个简单的用户侧实现大概长这样:
function PromiseDotResolve(value) { if (value instanceof Promise && value.constructor === Promise) { return value; return new Promise(resolve => resolve(value));
直接写 await 42 也是同样的道理。引擎会创建一个 promise 并立即调用 resolve(42),就像 Promise.resolve() 一样,同样的例外也适用。
很多人把 promise 理解为一个可能以两种方式结束的值——以某个值 fulfil,或以某个原因 reject。这是一个合理的近似,因为它与日常编写 JavaScript 时使用 promise 的实际感受非常接近。因此,一个朴素的 promise 实现可能长这样:
type PromiseState<T> =
| {type: 'pending'}
| {type: 'resolved', value: T}
| {type: 'rejected', value: unknown};
class MediocrePromise<T> {
private state: PromiseState<T> = {type: 'pending'};
private callbacks: Array<() => void> = []; // registered by .then()
private resolve(value: T) {
this.settle({type: 'resolved', value});
private reject(reason: unknown) {
this.settle({type: 'rejected', value: reason});
private settle(state: PromiseState<T>) {
this.state = state;
for (const callback of this.callbacks) callback();
// For example's sake this is missing constructor, then, etc.
我把它叫做 MediocrePromise。它有一个 pending 状态,有一个用值来 resolve 的方法,有一个用原因来 reject 的方法。它甚至有一个要运行的回调列表,就像真正的 promise 一样!这是一个好的实现吗?
很遗憾,并不是。这不是一个好的实现。MediocrePromise 遗漏了 resolving 和 rejecting 工作方式之间的一个重要区别。
让我们从 rejection 开始。Reject 一个 promise 很简单!调用 reject(reason) 会把 promise 标记为 rejected,以 reason 作为 rejected 的值。很好!MediocrePromise 正确地实现了 rejection!
Resolving 则更复杂。调用 resolve(value) 并不会直接存储 value。首先,它会检查 value 是否有 .then() 方法。如果没有(比如数字、字符串、没有该方法的对象……),promise 就以 value 被 fulfil,到此结束。否则,如果它确实有 .then() 方法,value 会被当作另一个 promise 对待。我们原来的 promise 还没有被 settle,取而代之的是,resolve 把自己和 reject 函数传给 value.then(resolve, reject)。value.then() 最终调用了这两个函数中的哪一个,就由哪个来 settle 这个 promise。抱歉,这有点绕口。马上有一张图来解释。请耐心听我说。
resolve 函数在规范中被定义为 resolve procedure。为了判断 value 是否有 .then,它会读取 value.then,而属性读取可能会触发 getter,而 getter 可能会抛出错误(就像我们开头那个程序一样)。所以,在某个值上查找 .then() 方法可能导致三种结果:
resolve(value)读取 value.then缺失,或不是函数以 value 进行 fulfilgetter 抛出错误以该错误进行 reject是一个函数跟随 value调用 value.then(resolve, reject)我们的 MediocrePromise 缺少了额外的检查和额外的行为!
这种不对称之所以存在,是因为失败不是一个 pending 中的操作。当某件事失败时,你已经在代码中有了原因(大概是一个错误)!所以你可以立即调用 reject(reason)。而 resolving 的顺利路径则不是这样,一个有价值的可能稍后才到达:
reject(new Error("bad request")); // the reason is right here resolve(response.json()); // the body is still arriving
这就是为什么额外的行为只在 resolve 中实现。
在 value.then 是一个函数的路径中,规范认为 value 是一个 thenable。Promise 是 thenable,因为它们有 .then() 方法。任何其他有 .then() 方法的对象也是 thenable。Thenable 在 promise 能用的任何地方都能用。
简单来说:thenable 就是任何带有 .then() 方法的值。因此,任何 promise 也是 thenable。
// thenable is not a promise!
const thenable = {
// resolve(thenable) calls this with the outer promise's resolve and reject
then(resolve, _reject) {
resolve(42);
};
// resolve() called with the thenable
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。