[翻译]  setTimeout / Promise.resolve: Macrotask vs Microtask

[CHINESE]  setTimeout / Promise.resolve:Macrotask vs Microtask


I've been introduced to the concepts of Microtasks and Macrotasks for a while now, and from everything I've read, I always thought setTimeout to be considered to create a macrotask and Promise.resolve() (or process.nextTick on NodeJS) to create microtasks.

我已经了解了Microtasks和Macrotasks的概念已有一段时间了,从我读过的所有内容来看,我一直认为setTimeout被认为是创建一个macrotask和Promise.resolve()(或NodeJS上的process.nextTick)创建微任务。

(Yes, I'm aware that different Promise libraries like Q and Bluebird have different schedulers implementations, but here I'm referring to the native Promises on each platform)

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

With this in mind I'm unable to explain the following sequence of events on NodeJS (results on Chrome are different from NodeJS (both v8 LTS and v10) and match with my understanding on this subject).

考虑到这一点,我无法解释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);
		});
	})
}

So, the results I have on Chrome (and that are consistent with my understanding of Micro/Macro tasks and how Promise.resolve and setTimeout behave) are:

因此,我在Chrome上的结果(与我对Micro / Macro任务的理解以及Promise.resolve和setTimeout的行为方式一致)是:

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

The same code executed on NodeJS outputs:

在NodeJS输出上执行的代码相同:

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

I'm looking for a way to have the same results on NodeJS that I have on Chrome. I've also tested with process.nextTick instead of Promise.resolve() but the results are the same.

我正在寻找一种在Chrome上使用NodeJS获得相同结果的方法。我还测试了process.nextTick而不是Promise.resolve(),但结果是一样的。

Can anyone point me into the right direction?

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

3 个解决方案

#1


1  

You can't control how different architectures queue the promises and timeouts.

您无法控制不同的体系结构如何排列承诺和超时。

Excellent Read Here: https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

优秀阅读:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

If you want the same results you are going to have to chain promises.

如果你想要相同的结果,你将不得不链接承诺。

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

#2


1  

Node.js doesn't really have concepts of micro and macro-tasks. And if you read about how it implements the event loop, then the behavior that you are describing makes perfect sense. Let me try to explain it.

Node.js实际上没有微观和宏观任务的概念。如果你阅读它是如何实现事件循环的,那么你所描述的行为就非常有意义。让我试着解释一下。

The event loop consists of different phases (like processing timers, I/O callbacks, etc), and each phase has a FIFO queue of callbacks to execute. On each event loop tick, node.js executes all callbacks one by one from the first phase in the order they were added, them moves to the next phase and repeats that process. So, when you add two timeouts, they are placed in the "timers phase" right next to each other. It means that any asynchronous operations in the first timer will be added to the end of the queue on this phase, or to the end of an another phase, or will be executed on entirely different event loop tick. In either case, the second timer will be executed before any other task, because it is how FIFO queues work.

事件循环由不同的阶段(如处理定时器,I / O回调等)组成,每个阶段都有一个要执行的回调FIFO队列。在每个事件循环tick上,node.js按照添加顺序从第一个阶段逐个执行所有回调,它们移动到下一个阶段并重复该过程。因此,当您添加两个超时时,它们将被放置在彼此相邻的“定时器阶段”中。这意味着第一个定时器中的任何异步操作都将在此阶段添加到队列的末尾,或者添加到另一个阶段的末尾,或者将在完全不同的事件循环时执行。在任何一种情况下,第二个计时器都将在任何其他任务之前执行,因为它是FIFO队列的工作方式。

Of course, it is possible to add your own implementation of macrotasks. Basically it requires ensuring that only one macrotask can be executed during one event loop cycle, and scheduling the next one for the next cycle. Try the following code, it should work as you expect:

当然,可以添加自己的macrotasks实现。基本上,它需要确保在一个事件循环周期中只能执行一个宏任务,并为下一个周期安排下一个宏任务。尝试以下代码,它应该按预期工作:

const taskQueue = [];
let nextTick = null;

function macrotask(f) {
  function executeTask() {
    const task = taskQueue.shift();

    if (!task) {
      return (nextTick = null);
    }

    nextTick = setImmediate(executeTask);
    const { f, args } = task;
    f(...args);
  }

  return function(...args) {
    taskQueue.push({ f, args });

    if (!nextTick) executeTask();
  };
}

const logs = i => {
  console.log('Timeout ', i);
  Promise.resolve()
    .then(() => console.log('Promise 1 ', i))
    .then(() => console.log('Promise 2 ', i));
};

const macroLogs = macrotask(logs);

for (let i = 0; i < 5; i++) {
  setTimeout(macroLogs, 0, i);
}

Note about inconsistencies in timeouts. Some people noticed that your example can produce different results on node.js. It is caused by the fact that

请注意超时中的不一致。有些人注意到你的例子可以在node.js上产生不同的结果。这是由于这一事实造成的

Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering.

Node.js不保证回调将触发的确切时间,也不保证它们的排序。

In short, Node.js delegates timers to the OS where they may end up on different threads and be called on different event loop cycles respectively, event if you schedule them with the same delays.

简而言之,Node.js将定时器委托给操作系统,它们可能分别在不同的线程上调用,并分别在不同的事件循环周期调用,如果您使用相同的延迟调度它们,则会发生事件。

#3


0  

I am not saying I got it right, I wrote something adhoc and I'd like you to test the below:

我不是说我做对了,我写了一些adhoc,我希望你测试下面的内容:

the wrapper:

包装器:

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;
}

to use:

使用:

create an instance:

创建一个实例:

var  x = new order;

then modify the rest a bit:

然后修改其余部分:

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

I get this:

我明白了:

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

You can even elaborate a bit:

你甚至可以详细说明一下:

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

In node v6, you get:

在节点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

Would you try this in your node version for me? In my node (6.11, I know its old) it works.

你会在我的节点版本中尝试这个吗?在我的节点(6.11,我知道它的旧)它的工作原理。

Tested on chrome, firefox, node v6.11

在chrome,firefox,node v6.11上测试过

Note: you don't have to keep reference to 'x', this within the pushed functions refer to the order instance. You can also use Object.defineProperties to render getters/setters unconfigurable, to prevent accidental deletion of instance.ignited etc.

注意:您不必保持对'x'的引用,这在推送的函数中引用了订单实例。您还可以使用Object.defineProperties来呈现不可配置的getter / setter,以防止意外删除instance.ignited等。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2018 ITdaan.com 粤ICP备14056181号