setTimeout/Promise.resolve: Macrotask vs Microtask

Я уже давно знаком с концепциями Microtasks и Macrotasks, и из всего, что я читал, я всегда считал, что setTimeout следует рассматривать как создание макрозадачи и Promise.resolve() (или process.nextTick на NodeJS) для создания микротоков.

(Да, я знаю, что разные библиотеки Promise, такие как Q и Bluebird, имеют разные реализации планировщиков, но здесь я имею в виду собственные обещания на каждой платформе)

Имея это в виду, я не могу объяснить следующую последовательность событий в 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);
		});
	})
}

Ответ 1

Для тех, кто ищет ответ на эту проблему. Это было признано командой NodeJs как ошибка, более подробно здесь: https://github.com/nodejs/node/issues/22257

Между тем это было уже исправлено и выпущено, имеет часть Node v11.

Бест, Хосе

Ответ 2

Вы не можете контролировать, как разные архитектуры ставят в очередь обещания и тайм-ауты.

Отлично читайте здесь: 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'))

Ответ 3

Я не говорю, что все правильно, я написал что-то adhoc, и я бы хотел, чтобы вы протестировали следующее:

обертка:

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

В узле 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

Вы бы попробовали это в своей версии для меня? В моем узле (6.11, я знаю его старый) он работает.

Протестировано на chrome, firefox, node v6.11

Примечание: вам не нужно ссылаться на "x", this пределах нажатых функций относится к экземпляру order. Вы также можете использовать Object.defineProperties для рендеринга Object.defineProperties/Object.defineProperties, чтобы предотвратить случайное удаление instance.ignited и т.д.