Updating DOM before blocking code by setTimeout or promise

爷,独闯天下 提交于 2019-12-05 16:56:55
Benjamin Gruenbaum

Promise.prototype.then has microtask semantics. This means it has to wait for synchronous code to run but not for asynchronous code to run - browsers probably choose to wait for all JS to run before doing DOM updates.

Generally microtask means it has to wait for other JS to run, and then it can run before yielding control to non JS code.

setTimeout has macrotask semantics. It runs as a part of the DOM API and when the callback runs the non-js code has already gotten a chance to run. Browsers already run their own code when this runs so they also process events and DOM updates.

Generally macrotask means that it has to wait for all other JS to run and also for the "event loop to tick" - that is: events to fire.

This is also the difference between setImmediate and nextTick in NodeJS.

To answer your question directly: no. There is no way to force the browser to run DOM updates in a microtick update - while it is technically not forbidden for it to do so - it would be "bad mannered".

For long running CPU bound operations - may I suggest Web Workers instead?

The problem is that the promise, even if it runs asynchronously, runs too early. So browsers don't have time to update the DOM. This problem is not specific to promises, I see the same result when using a setTimeout with a 0ms delay:

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // we will never see this
setTimeout(_ => blockFor(2000), 0);      // 0ms is not enough
<p id="result"></p>

In fact, it seems what you want is requestAnimationFrame:

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // now you see me
new Promise(function(resolve) {
  requestAnimationFrame(_ => resolve(2000));
}).then(blockFor);
<p id="result"></p>

But at this point you could use requestAnimationFrame alone, without promises.

function blockFor(dur){
  var now = new Date().getTime();
  while (new Date().getTime() < now + dur);
  result.textContent = "I am done..!";
}
result.textContent = "Please remain..."; // now you see me
requestAnimationFrame(_ => blockFor(2000));
<p id="result"></p>

Best way to do it is to delegate the heavy process to a web worker...

// main thread

document.getElementById("result").addEventListener('click', handleClick);
const worker = new Worker('worker.js');


function handleClick(){
  worker.onmessage = e => {
   console.log('main', e.data.response)  
   this.textContent = e.data.response;
  }
  this.textContent = "Please remain...";
  worker.postMessage({data: 2000});
}

// worker

self.addEventListener('message', e => {
    const { data } = e.data;
    console.log('worker', data); 

    function blockFor(dur){
     var now = new Date().getTime();
     while (new Date().getTime() < now + dur);
     }

    blockFor(data)
    self.postMessage({ response: "I am done..!" });
});



  // NOTE: perform this test on your app for browser compatibility
  if (window.Worker) {
  ...

}

Check out this live code

MDN web workers docs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!