为什么以下原因导致堆栈溢出?

时间:2020-03-05 16:07:19

标签: 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)

3 个答案:

答案 0 :(得分:3)

此递归功能完全在同一个微任务中解决。

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

但是,每次执行wait都无法在调用下一个await之前获得对wait的承诺。因此,实际上堆栈是在没有调度微任务的情况下同步溢出的。您实际上从未获得过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()微任务运行一段时间后被调用。

答案 1 :(得分:1)

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');

答案 2 :(得分:0)

该功能没有完成阻止任务。因此,它将几乎立即继续执行下一个递归,并且由于没有递归转义,因此不会导致堆栈溢出。

在代码中引入诸如控制台日志之类的耗时任务,它将运行良好。

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)