Why does setting CSS property using Promise.then not actually happen at the then block?

Richard :

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:

  • Click happens
  • Box starts translating horizontally by 100px (this action takes two seconds)
  • On click, a new Promise is also created. Inside said Promise, a setTimeout function is set to 2 seconds
  • After the action is completed (two seconds have elapsed), setTimeout 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.
  • The box appears at the original location with no transition effect problem here
  • After all of those finish, set the transition value of the box back to its original value

However, 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: Problem

CertainPerformance :

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>

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=9381&siteId=1