1

Why doesn't the background change right as I copy? I added a console.log() and as you can see, the console.log() works, but the background won't change. What is the problem here?

To test, click on the snippet and then press CMD + C (Windows:CTRL + C)

window.addEventListener('copy', function(e) {
  e.preventDefault();

  //This should change!
  document.getElementById("object").style.backgroundColor = 'white';
  console.log("Started!");

  tryCopyAsync(e).then(() =>
    document.getElementById("object").style.backgroundColor = 'gray'
  );
});
async function tryCopyAsync(e){
  if(navigator.clipboard){
    await e.clipboardData.setData('text/plain',getText());
  }
}
function getText(){
  var html = '';
  var row = '<div></div>';
  for (i=0; i<100000; i++) {
      html += row;
  }
  return html;
}
#object{
  width:100%;
  height:100vh;
  background:gray;
}
body{
  padding:0;
  margin:0;
  overflow:hidden;
}
<div id='object'></div>
Seth B
  • 1,014
  • 7
  • 21
  • 3
    I believe the browser batches DOM updates but it can't start a repaint until the UI thread is available, which in turn is blocked by your `getText` and `sleepFor` functions. Read up on microtasks, macrotasks and `requestAnimationFrame`. – Felix Kling Feb 11 '21 at 22:40
  • I'll checkout that – Seth B Feb 11 '21 at 22:43
  • 1
    I tried to wrap `tryCopyAsync(e).then...` in a `setTimeout` without any second argument and it works. But no idea what's exactly going on and it's not a very clean solution. Felix Kling might be on the right track here. – Getter Jetter Feb 11 '21 at 22:45
  • Yes it does work, but I would like to know why. – Seth B Feb 11 '21 at 22:55
  • 1
    single threaded..... https://stackoverflow.com/questions/55347558/is-there-a-way-to-update-dom-element-from-within-a-for-loop your while loop is not really a sleep, it just locks up the browser. – epascarello Feb 12 '21 at 01:04
  • @epascarello I removed the setTimeout, now the console.log() works, but the background still won't change. – Seth B Feb 12 '21 at 02:06

2 Answers2

2

First, your sleepFor method is completely blocking the event-loop synchronously, even if it is called from an async function:

window.addEventListener('copy', function(e) {
  e.preventDefault();

  //This should change!
  document.getElementById("object").style.backgroundColor = 'white';
  console.log("Started!");

  tryCopyAsync(e).then(() =>
    document.getElementById("object").style.backgroundColor = 'gray'
  );
  console.log('sync');
});
async function tryCopyAsync(e){
  if(navigator.clipboard){
    await e.clipboardData.setData('text/plain',getText());
  }
}
function sleepFor(sleepDuration){
  var now = new Date().getTime();
  while(new Date().getTime() < now + sleepDuration){} 
}
function getText(){
  console.log('blocking');
  var html = '';
  var row = '<div></div>';
  for (i=0; i<10; i++) {
      html += row;
      sleepFor(300);
  }
  console.log('stopped blocking');
  return html;
}
onclick = e => document.execCommand('copy');
#object{
  width:100%;
  height:100vh;
  background:gray;
}
body{
  padding:0;
  margin:0;
  overflow:hidden;
}
click to trigger the function
<div id='object'></div>

But even if it were called in a microtask, that wouldn't change a thing, because microtasks also do block the event-loop. (Read that linked answer, it explains how the rendering is tied to the event-loop).

If what you want is to have your code let the browser do its repaints, you need to let the browser actually loop the event-loop, and the only ways to do this are:

  • split your getText logic and make it wait for the next event-loop iteration by posting a task (e.g through setTimeout)
  • use a dedicated Worker to produce the data returned by getText.

However beware you were not using the async Clipboard API, but simply overriding the default value of the copy event, which can not be done asynchronously. So going this way you will actually need to really use the Clipboard API.

Here is an example using a MessageChannel to post a task since current stable Chrome still has a 1ms minimum delay for setTimeout:

window.addEventListener('copy', function(e) {
  e.preventDefault();

  //This should change!
  document.getElementById("object").style.backgroundColor = 'white';
  console.log("Started!");

  tryCopyAsync(e).then(() =>
    document.getElementById("object").style.backgroundColor = 'gray'
  );
});
async function tryCopyAsync(e) {
  if (navigator.clipboard) { // you were not using the Clipboard API here
    navigator.clipboard.writeText(await getText());
  }
}
async function getText() {
  var html = '';
  var row = '<div></div>';
  for (i = 0; i < 1000000; i++) {
    if (i % 1000 === 0) { // proceed by batches of 1000
      await waitNextFrame();
    }
    html += row;
  }
  return html;
}

function waitNextFrame() {
  return new Promise(postTask);
}

function postTask(task) {
  const channel = postTask.channel ||= new MessageChannel();
  channel.port1.addEventListener("message", () => task(), {
    once: true
  });
  channel.port2.postMessage("");
  channel.port1.start();
}
onclick = (evt) => document.execCommand("copy");
#object {
  width: 100%;
  height: 100vh;
  background: gray;
}

body {
  padding: 0;
  margin: 0;
  overflow: hidden;
}
<div id='object'></div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • I edited my code, now the console.log works, but the background still won't. – Seth B Feb 12 '21 at 02:05
  • 1
    @SethB did you read my answer until the end? You neither did split your getText method and make it wait for a new event-loop iteration, nor did you move the computation to a Worker. Please also read [this answer](https://stackoverflow.com/questions/62562845/any-example-proving-microtask-is-executed-before-rendering/62567012#62567012). I did edit my answer with a live example. – Kaiido Feb 12 '21 at 03:12
0

Well, firstly, the argument for "then" should be a function.

.then(()=>{})

From running your code, it looks like this

document.getElementById("object").style.backgroundColor = 'gray'; 

is getting called immediately after setting the background color white, so you aren't able to notice it turning white, even though it is (just very very briefly).

Try setting some logging in your tryCopyAsync function to see why it is finishing too quickly.

Jen H
  • 147
  • 8
  • I actually isn't calling document.getElementById("object").style.backgroundColor = 'gray'; right away – Seth B Feb 12 '21 at 00:56