Please try and run the following snippet, then click on the box.
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
new Promise(resolve => {
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
resolve('Transition complete')
}, 2000)
}).then(() => {
box.style.transition = ''
})
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
What I expect to happen:
Promise
is also created. Inside said Promise
, a setTimeout
function is set to 2 secondssetTimeout
runs its callback function and set transition
to none. After doing that, setTimeout
also reverts transform
to its original value, thus rendering the box to appear at the original location.transition
value of the box back to its original valueHowever, as can be seen, the transition
value does not seem to be none
when running. I know that there are other methods to achieve the above, e.g. using keyframe and transitionend
, but why does this happen? I explicitly set the transition
back to its original value only after the setTimeout
finishes its callback, thus resolving the Promise.
EDIT
As per request, here's a gif of the code displaying the problematic behaviour:
Return valuereturns a value, the promise returned by then gets resolved with the returned value as its value. doesn't return anything, the promise returned by then gets resolved with an undefined value. throws an error, the promise returned by then gets rejected with the thrown error as its value.
The finally() method of a Promise schedules a function, the callback function, to be called when the promise is settled. Like then() and catch() , it immediately returns an equivalent Promise object, allowing you to chain calls to another promise method, an operation called composition.
A promise is a pattern for handling asynchronous operations. The promise allows you to call a method called "then" that lets you specify the function(s) to use as the callbacks.
The event loop batches style changes. If you change the style of an element on one line, the browser doesn't show that change immediately; it'll wait until the next animation frame. This is why, for example
elm.style.width = '10px';
elm.style.width = '100px';
doesn't result in flickering; the browser only cares about the style values set after all Javascript has completed.
Rendering occurs after all Javascript has completed, including microtasks. The .then
of a Promise occurs in a microtask (which will effectively run as soon as all other Javascript has finished, but before anything else - such as rendering - has had a chance to run).
What you're doing is you're setting the transition
property to ''
in the microtask, before the browser has started rendering the change caused by style.transform = ''
.
If you reset the transition to the empty string after a requestAnimationFrame
(which will run just before the next repaint), and then after a setTimeout
(which will run just after the next repaint), it'll work as expected:
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
// resolve('Transition complete')
requestAnimationFrame(() => {
setTimeout(() => {
box.style.transition = ''
});
});
}, 2000)
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class="box"></div>
You are facing a variation of the transition doesn't work if element start hidden problem, but directly on the transition
property.
You can refer to this answer to understand how the CSSOM and the DOM are linked for the "redraw" process.
Basically, browsers will generally wait until the next painting frame to recalculate all the new box positions and thus to apply CSS rules to the CSSOM.
So in your Promise handler, when you reset the transition
to ""
, the transform: ""
has still not been calculated yet. When it will get calculated, the transition
will already have been reset to ""
and the CSSOM will trigger the transition for the transform update.
However, we can force the browser to trigger a "reflow" and thus we can make it recalculate the position of your element, before we reset the transition to ""
.
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
new Promise(resolve => {
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
box.offsetWidth; // this triggers a reflow
resolve('Transition complete')
}, 2000)
}).then(() => {
box.style.transition = ''
})
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
Which makes the use of the Promise quite unnecessary:
const box = document.querySelector('.box')
box.addEventListener('click', e => {
if (!box.style.transform) {
box.style.transform = 'translateX(100px)'
setTimeout(() => {
box.style.transition = 'none'
box.style.transform = ''
box.offsetWidth; // this triggers a reflow
// even synchronously
box.style.transition = ''
}, 2000)
}
})
.box {
width: 100px;
height: 100px;
border-radius: 5px;
background-color: #121212;
transition: all 2s ease;
}
<div class = "box"></div>
And for an explanation on micro-tasks, like Promise.resolve()
or MutationEvents, or queueMicrotask()
, you need to understand they'll get ran as soon as the current task is done, 7th step of the Event-loop processing model, before the rendering steps.
So in your case, it's very like if it were ran synchronously.
By the way, beware micro-tasks can be as blocking as a while loop:
// this will freeze your page just like a while(1) loop
const makeProm = ()=> Promise.resolve().then( makeProm );
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With