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>