为什么嵌套的 postMessage 会导致 rAF 在 setTimeout 之前触发?

Why nested postMessage will cause rAF fired before setTimeout?

代码:

var mc = new MessageChannel();
var count = 1;
var t = +new Date;


console.log('start')

Promise.resolve().then(() => console.log('promise'))
requestAnimationFrame(() => console.log('animationframe'))

setTimeout(() => console.log('timeout'))

mc.port1.onmessage = (...arr) => {
  console.log('ommessage')

  // this will cause the setTimeout fired **before** rAF
  if (count === 1) { mc.port2.postMessage(null); count = 2 }

  // I guess maybe because we spend too much time so the rAF be deffered? But that's not true because if I remove the upper code and use below code won't happen that
  // while(+new Date - t < 3000) {}
  // or
  // Promise.resolve().then(() => while(+new Date - t < 3000))
}

mc.port2.postMessage(null);

console.log('end')

如果您删除嵌套的 postMessage,它将正常工作。那么,嵌套的poseMessage为什么会改变事件循环的执行顺序呢?

顺便说一句,增加 setTimeout 的延迟也会使其正常工作,这意味着它仍然是一个 "time we spend" 问题?

这里似乎对 requestAnimationFrame 的作用以及所有这些事件应该如何发生存在一些误解。

所以首先,requestAnimationFrame(callback)callback 存储在 map of animation-callbacks. This map will get emptied and all its callbacks called at the next painting frame 中(第 10 步)。
绘画框架 是 HTML 规范未明确定义何时应有意发生的东西,实施者确实根据自己的启发式方法决定何时最好绘画。例如,Firefox 将每 60 秒调用一次,另一方面,Chrome 将以与显示页面的监视器相同的速率调用它。

所以如果我们从外在的眼光来看,我们可以说requestAnimationFrame就是一个setTimeout(callback, time_until_next_painting_frame)

但是,这些回调都将在事件循环接近尾声时被调用。

另一方面,事件将在事件循环的开始处处理。特别是消息事件是触发 queue a task algorithm, since it's actually done synchronously.
的最快方式 虽然定时器总是会在之后至少触发两次事件循环迭代:

  1. 声明定时器
  2. 如果时间已过,请排队任务
  3. 触发任务

这就是为什么 onmessage 应该在超时前触发(尽管从技术上讲,延迟超时可能会在 onmessage 之前触发)。

现在我们可以来看看 Promise.resolve() 创建的最后一个野兽,microtasks。这些只是在等待当前任务的当前执行。也就是说,确实调用了 Promise 的 resolve 方法的回调、事件处理程序或内联脚本,或者确实进行了触发 MutationObserver 事件的更改。

也许为此,一个简单的例子胜过千言万语:

const channel = new MessageChannel();
channel.port1.onmessage = ({ data }) => {
  console.log(`enter message ${data} event handler`);
  Promise.resolve()
    .then( () => {
      console.log(`enter microtask ${data} event handler`);
      Promise.resolve()
        .then( () => 
          console.log(`enter sub-microtask ${data} event handler`)
        )
      // we could go into an infinite loop here
      // which would be as blocking as a while loop
      // because we never leave the current task
    } )
};

// both events should happen in the same Event-Loop iteration
channel.port2.postMessage(1);
channel.port2.postMessage(2);

有了所有这些,我们可以为您的代码提供一个不太高级的语言版本:

setPolyfills(); // so we can talk less high language

console.log('start');

queueMicroTask(() => console.log('promise'));

// it's basically random,
// though here we omit the very special case of
// *call-at-beginning-of-painting-frame*
setTimeout(() => console.log('animationframe'), Math.random() * 16.6);

setTimeout(() => console.log('timeout'));

queueATask(() => {
  console.log('ommessage');
  queueATask(() => {
    console.log('onmessage');
  });
});

console.log('end');


// low-level helpers
function setPolyfills() {
  // Chrome has a native
  if( !('queueMicroTask' in window) ) {
    window.queueMicroTask = (fn) => Promise.resolve().then(fn);
  }
  window.queueATask = (fn) => {
    const channel = new MessageChannel();
    channel.port1.onmessage = e => fn();
    channel.port2.postMessage('');
  };
}

鉴于我们之前所说的,我们有理由期待像

这样的输出
// first event-loop iteration
start       // synchronous
end         // synchronous
promise     // micro-task

// second event-loop iteration
ommessage   // beginning of second event-loop iteration
//...

然后是 timeoutonmessageanimationframe,顺序随机。

timeout 可以在 onmessage 之前触发,具体取决于检查计时器的时间,它可以在第二次迭代或第三次迭代中尽快触发。
onmessage 应该在第三次迭代中触发。
animationframe 可以在任何迭代中触发,从第一个 到任何其他迭代,直到下一个绘画帧。实际上,由于它实际上是在事件循环迭代结束时触发的,因此您甚至可以在消息事件之前触发它。

尽管这种非常罕见的幸运地从画框的开头调用它的情况应该很少发生。但是 Chrome has a running bug 从非动画文档第一次调用 requestAnimationFrame 会立即被调用,即使这个帧实际上不是 绘画帧 ...我担心你在做测试时也遇到过这个问题。

因此,如果我们现在应用我在此错误报告中提出的解决方法,我们也可以在 Chrome 中获得更稳定的结果:

// workaround crbug 919408 by running an infinite rAF loop
const anim = () => requestAnimationFrame(anim);
anim();
// we thus need to wait it's warmed up
setTimeout(() => { 
  var mc = new MessageChannel();
  var count = 1;
  var t = +new Date;


  console.log('start')

  Promise.resolve().then(() => console.log('promise'))
  requestAnimationFrame(() => console.log('animationframe'))

  setTimeout(() => console.log('timeout'))

  mc.port1.onmessage = (...arr) => {
    console.log('ommessage')
    if (count === 1) { mc.port2.postMessage(null); count = 2 }
  }

  mc.port2.postMessage(null);

  console.log('end')
}, 500);