为什么要使用process.nextTick来保证异步任务的正确执行?

Why use process.nextTick to ensure correct execution of asynchronous tasks?

我正在按照一本书 (Node.js design patterns) 的示例在 Node.js 和 Javascript 中实现 LIMITED PARALLEL EXECUTION 算法。

首先,我编写了一个 TaskQueue class 来处理任务的所有限制和执行。

class TaskQueue {
    constructor(concurrency) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }
    runTask(task) {
        return new Promise((resolve, reject) => {
            this.queue.push(() => {
                return task().then(resolve, reject);
            });
            process.nextTick(this.next.bind(this));
        });
    }
    next() {
        while (this.running < this.concurrency && this.queue.length) {
            const task = this.queue.shift();
            task().finally(() => {
                this.running--;
                this.next();
            });
            this.running++;
        }
    }
}

然后我写了一个函数,returns 一个在随机数毫秒后解析的承诺,并用分配的数字记录承诺。

此函数将任务委托给队列,将承诺包装在将由 TaskQueue 按需执行的函数中 class。

function promiseResolveRandomValue(promiseNumber, queue) {
    return queue.runTask(() => {
        return new Promise(resolve => {
            const random = Math.round(Math.random()*1000);
            setTimeout(() => {
                console.log('Resolved promise number: ', promiseNumber, ' with delay of: ', random);
                resolve();
            }, random);
        });
    })
}

最后,我使用了一个 map 在十个数字的数组上执行函数,这将帮助我们跟踪每个承诺。

const queue = new TaskQueue(2);
const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const promises = array.map((number) => {
    return promiseResolveRandomValue(number, queue);
});

Promise.all(promises)
    .then(() => console.log('Finished'));

到目前为止一切顺利。问题是作者建议用 process.nextTick 执行这行代码以避免任何 Zalgo 类型的情况,但我不明白为什么,因为这段代码执行完全相同只是调用 next 的方法class.

runTask(task) {
        return new Promise((resolve, reject) => {
            this.queue.push(() => {
                return task().then(resolve, reject);
            });
            process.nextTick(this.next.bind(this)); // <-------------------------------- WHY?
        });
    }

有人能解释一下为什么在某些情况下使用 process.nextTick 可以方便地避免与同步和异步代码发生冲突吗?

作者所指的 Zalgo situation

console.log("Before scheduling the task")
queue.runTask(() => {
    console.log("The task actually starts");
    return new Promise(resolve => {
        // doesn't really matter:
        setTimeout(resolve, Math.random()*1000);
    });
});
console.log("Scheduled the task");

如果您没有使用 nextTick,日志可能会以不同的顺序出现 - 任务实际上可能在 runTask() 函数 returns 之前开始,具体取决于那里有多少已经在队列中了。

这可能并不那么出乎意料 - 毕竟,我们期望 runTask 到 运行 任务 - 但是,如果任务实际上同步开始并带有一些副作用,那可能是出乎意料的。这种副作用的一个例子是 next 方法本身 - 考虑以下内容:

queue.runTask(() => …);
console.log(`Scheduled the task, there are now ${queue.queue.length} tasks in the queue`);

您是否希望长度始终至少为 1?避免 Zalgo 可以提供这样的保证。

(另一方面,如果我们改为记录 queue.running,期望它在调用 runTask 后立即 >=1 对我来说似乎同样明智,并且需要开始 next 同步。清晰的文档在这里胜过任何直觉。)