setTimeout / Promise.resolve: 宏任务 vs 微任务

14

我对Microtasks和Macrotasks的概念有所了解,根据我所读到的内容,我一直认为setTimeout被视为创建一个macrotask,而Promise.resolve()(或者在NodeJS上是process.nextTick)被视为创建microtask。

(是的,我知道不同的Promise库(例如Q和Bluebird)有不同的调度程序实现,但这里我指的是每个平台上的原生Promises)

考虑到这一点,我无法解释在NodeJS上发生的以下事件序列(Chrome上的结果与NodeJS(v8 LTS和v10)不同,并且与我对该主题的理解相匹配)。

for (let i = 0; i < 2; i++) {
 setTimeout(() => {
  console.log("Timeout ", i);
  Promise.resolve().then(() => {
   console.log("Promise 1 ", i);
  }).then(() => {
   console.log("Promise 2 ", i);
  });
 })
}

因此,我在Chrome上得到的结果(并且这与我的微观/宏观任务和Promise.resolve以及setTimeout行为的理解一致)是:

Timeout  0
Promise 1  0
Promise 2  0
Timeout  1
Promise 1  1
Promise 2  1

相同的代码在NodeJS上执行的输出如下:
Timeout  0
Timeout  1
Promise 1  0
Promise 2  0
Promise 1  1
Promise 2  1

我正在寻找一种在NodeJS上能够得到与Chrome相同结果的方法。我也已经尝试使用process.nextTick替代Promise.resolve(),但结果相同。

有人能指导我正确的方向吗?


你是想更好地理解还是在实际代码中遇到了问题? - Bergi
@Bergi 我实际上尝试过了。无论时间如何,同样的问题都会发生。是的,我有一个“真正的代码”问题。不幸的是,示例本身有点复杂,我已经将其简化为我在问题中发布的代码。最终目标是我需要将一个对象与事件循环中发生的所有事情同步。这样,在下一个循环中,我就可以将其序列化并发送到其他地方。如果Node的行为像Chrome一样,我可以轻松实现这一点。 - jpsfs
我发布了一个有效的代码片段,可以解决你的问题。如果你在循环中创建超时,会得到不一致的结果。 - Steven Spungin
您需要创建一个包装器来返回一个带有setTimeout结果的对象,并嵌入承诺的回调函数。 - ibrahim tanyalcin
1
基本上,你要求的是强制按顺序运行异步代码,使用async / await很容易实现这一点。 - James
显示剩余8条评论
3个回答

2

1
你无法控制不同架构如何排队承诺和超时。
优秀文章请参阅:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ 如果您想要相同的结果,您将需要链接承诺。

let chain = Promise.resolve(null)

for (let i = 0; i < 2; i++) {
  console.log("Chaining ", i);
  chain = chain.then(() => Promise.resolve()
    .then(() => {
      setTimeout(() => {
        console.log("Timeout ", i);

        Promise.resolve()
          .then(() => {
            console.log("Promise 1 ", i);
          })
          .then(() => {
            console.log("Promise 2 ", i);
          })

      }, 0)
    }))
}

chain.then(() => console.log('done'))


谢谢你的回答。这只是一个简化的例子。我不能改变for循环内的setTimeout。我可以改变setTimeout内的Promise.resolve()。 - jpsfs
请查看工作代码片段。Node和Chrome在循环中处理0延迟的超时有所不同。 - Steven Spungin
谢谢你的帮助。 实际上问题不在于setTimeout,而是在于宏/微任务。我需要在宏任务中排队一个微任务,以便在下一个宏任务之前执行该微任务。你知道如何在NodeJS中安排一个宏任务吗(我认为setTimeout会这样做,但不幸的是似乎并不是这样)? - jpsfs
我发布的Jake的文章解释了为什么你会看到这种行为以及如何处理。如果你想通过使用宏/微来优化性能,也许问题应该是如何处理即使顺序不同的结果... - Steven Spungin
@jpsfs 我在链中设置了超时。现在它们在节点和Chrome上的输出结果都相同。可能不是理想的或者你想要的,但它解决了这个问题。 - Steven Spungin
显示剩余5条评论

0

我并不是说我做得对,我写了一些临时的东西,我希望你能测试以下内容:

包装器:

function order(){
    this.tasks = [];
    this.done = false;
    this.currentIndex = 0;
    this.ignited = false;
}
order.prototype.push = function(f){
    var that =  this,
        args = Array.prototype.slice.call(arguments).slice(1);
    if(this._currentCaller){
        this.tasks.splice(
            this.tasks.indexOf(this._currentCaller) + 1 + (this.currentIndex++),
            0,
            function(){that._currentCaller = f; f.apply(this,args);}
        );
    } else {
        this.tasks.push(function(){that._currentCaller = f; f.apply(this,args);});
    }
    !this.ignited && (this.ignited = true) && this.ignite();
    return this;
}
order.prototype.ignite = function(){
    var that = this;
    setTimeout(function(){
        if(that.tasks.length){
            that.tasks[0]();
            that.tasks.shift();
            that.repeat(function(){that.reset(); that.ignite()});
        } else {
            that.ignited = false;
            that.reset();
        }
    },0);
}
order.prototype.repeat = function(f){
    var that = this;
    if(this.done || !this.tasks.length){
        f();
    } else {
        setTimeout(function(){that.repeat(f);},0);
    }
}
order.prototype.reset = function(){
    this.currentIndex = 0; 
    delete this._currentCaller; 
    this.done = false;
}

使用方法:

创建一个实例:

var  x = new order;

然后稍微修改一下剩下的部分:

for (let i = 0; i < 2; i++) {
    x.push(function(i){
        setTimeout(() => {
            console.log("Timeout ", i);
            x.push(function(i){
                Promise.resolve().then(() => {
                    console.log("Promise 1 ", i);
                }).then(() => {
                    console.log("Promise 2 ", i);
                    x.done = true;
                })
            },i);
            x.done = true;
        });
    },i);
}

我得到了这个:
Timeout  0
Promise 1  0
Promise 2  0
Timeout  1
Promise 1  1
Promise 2  1

你甚至可以详细阐述一下:

for (let i = 0; i < 2; i++) {
    x.push(function(i){
        setTimeout(() => {
            console.log("Timeout ", i);
            x.push(function(i){
                Promise.resolve().then(() => {
                    console.log("Promise 1 ", i);
                }).then(() => {
                    console.log("Promise 2 ", i);
                    x.done = true;
                })
            },i)
            .push(function(i){
                Promise.resolve().then(() => {
                    console.log("Promise 1 ", i);
                }).then(() => {
                    console.log("Promise 2 ", i);
                    x.done = true;
                })
            },i+0.5)
            .push(function(i){
                Promise.resolve().then(() => {
                    console.log("Promise 1 ", i);
                }).then(() => {
                    console.log("Promise 2 ", i);
                    x.done = true;
                })
            },i+0.75);
            x.done = true;
        });
    },i);
}

在Node v6中,您会得到:
Timeout  0
Promise 1  0
Promise 2  0
Promise 1  0.5
Promise 2  0.5
Promise 1  0.75
Promise 2  0.75
Timeout  1
Promise 1  1
Promise 2  1
Promise 1  1.5
Promise 2  1.5
Promise 1  1.75
Promise 2  1.75

你能帮我在你的Node版本中尝试一下吗?在我的Node(6.11,我知道它有点旧)上可以运行。

已在Chrome、Firefox和Node v6.11上测试通过

注意:你不必保留对'x'的引用,在推送的函数中使用this来引用order实例。你还可以使用Object.defineProperties将getter/setter设置为不可配置,以防止意外删除instance.ignited等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接