为什么以下内容会导致堆栈溢出

  • 本文关键字:堆栈 栈溢出 javascript
  • 更新时间 :
  • 英文 :


以下函数旨在通过使用await重复创建微任务来阻止宏任务。

但是为什么它会导致堆栈溢出呢?

我认为await会将递归调用放在微任务上,从而清空堆栈(显然我错了(。

const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    const wait = async () => {
        let elapsed = (performance.now() - start)
        if(elapsed < durationMs) await wait()
    }
    await wait()
}
waitUsingMicroTasks(1000)

而这一点——我认为几乎是等效的——并没有导致堆叠式气流:

const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    const wait = async () => {
        let elapsed = (performance.now() - start)
        if(elapsed < durationMs) Promise.resolve().then(wait)
    }
    await wait()
}
waitUsingMicroTasks(1000)

这个递归函数完全在同一个微任务中解析。

当遇到await时,会创建一个新的微任务,这是正确的。但是await运算符需要有一个Promise或值,以便隐式地将其包装在一个新的Promise中并附加回调。这意味着awaited的值需要首先进行评估,然后才能安排为微任务。

然而,每次执行wait都无法在调用下一个wait之前获得对await的Promise。因此,实际上堆栈正在同步溢出,而没有调度任何微任务。您永远不会真正获得Promise,因为每个Promise都依赖于下一个要评估的Promise——每个wait()调用都同步发生在任何await被解析之前的最后一个调用之后。

您可以通过await输入一些不需要计算递归调用的值来强制执行微任务:

const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    const wait = async () => {
        let elapsed = await (performance.now() - start)
        if(elapsed < durationMs) await wait()
    }
    await wait()
}
waitUsingMicroTasks(1000)

在第二个例子中,这个问题并不存在,因为您显式地创建了一个Promise,并将wait作为回调附加到它。这样,wait不会立即执行(就像await一样(,但它会在一段时间后运行Promise.resolve((微任务时被调用。

Klaycon的回答几乎涵盖了所有内容,但很容易观察到您对wait的所有调用都是同步发生的(在堆栈上(:

const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    let i = 0;
    const wait = async () => {
        console.log(i);
        i += 1;
        (i < 20) &&  (await wait());
    }
    wait();
    console.log('done');
}
waitUsingMicroTasks(5000)

您可以在这里看到,所有的console.log(i)都发生在console.log('done')之前,因此wait()根本不是异步运行的。

正如Klaycon所指出的,等待不是对wait递归调用的东西似乎可以解决这个问题,但你可以采取的另一种方法是等待实际上异步的东西,比如setTimeout:

const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    let i = 0;
    console.log('starting', new Date());
    while ((performance.now() - start) < durationMs) {
        await Promise.resolve();
    }
    console.log('done', new Date());
}
waitUsingMicroTasks(5000);
console.log('after wait');

函数中没有执行阻塞任务。因此,它将几乎立即执行下一次递归,并且由于没有递归转义,因此会导致堆栈溢出。

在代码中引入像控制台日志这样耗时的任务,它就会运行良好。

let count=0;
const waitUsingMicroTasks = async (durationMs, start = performance.now()) => {
    const wait = async () => {
        let elapsed = (performance.now() - start);
        console.log(++count);
        (elapsed < durationMs) &&  (await wait());
    }
    await wait();  
}
waitUsingMicroTasks(1000)

最新更新