简体   繁体   中英

requestAnimationFrame and getComputedStyle don't work as expected (following along Jake Archibalds "In the Loop" speech)

I am watching and following along Jake Archibald's speech about the Event Loop. At some point he attemps to move a box, first to 1000px and then back to 500px. You can also watch in the video here:

Jake Archibald: In The Loop - JSConf.Asia (22:10)

He offers two solutions and I couldn't get any of them to work for me, even though (I think) I am doing exactly the same. The first solution is this:

button.addEventListener("click", () => {
box.style.transform = "translateX(1000px)";
box.style.transition = "transform 1s ease-in-out";

requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    box.style.transform = "translateX(500px)";
  });
 });
});

And the second "hacky" one, as he says, is this:

button.addEventListener("click", () => {
   box.style.transform = "translateX(1000px)";
   box.style.transition = "transform 1s ease-in-out";
   getComputedStyle(box).transform;
   box.style.transform = "translateX(500px)";

});

On both occasions the box just moves to 500px, while it should first move to 1000px and then back to 500px.

Am I missing something? Am I doing something wrong? Or have things changed that much since 2018 so that this solution doesn't work anymore?

PS I am using Chrome Version 102.0.5005.61 on Ubuntu 22.04.

What the video shows is wrong here.

For this explanation I will use the "hacky" way, since it seems easier to understand as it's synchronous and thus we don't have to wonder when the reflow happens. (And if you know what you're doing it's not actually more hacky than any async way).

For a transition to work, we need an initial value, a destination and a flag telling we're ok to transition a given property change.
When this flag is raised, any change to the value that can be transitioned will be used as the destination, and the current value will be used as the initial value of our transition.

In the example, before we click the button, we only have an initial value ( none , ie translateX(0px) ).

button.addEventListener("click", () => {

Then we set both the new transform and transition values.

   box.style.transform = "translateX(1000px)";
   box.style.transition = "transform 1s ease-in-out";

At this point, the CSS layout hasn't been updated, it doesn't know we did change anything.
The next line, which forces the reflow, will take care of that.

   getComputedStyle(box).transform;

And here the CSS engine sees that it has the transition flag for transform raised AND that the transform value has changed. It thus initiates a transition from the current value ( 0px ) to the new one ( 1000px ).
But even before it can start rendering that transition,

   box.style.transform = "translateX(500px)";

changes the destination to be 500px . So it will update the transition to go from wherever it is at this point (still 0px ) to that new destination ( 500px ).

The same obviously happens with the async version, except that when we set the new value to 500px our element will already have moved a bit (by ~17px on a 60Hz monitor, while it should have only moved by half of that).
This isn't very noticeable with this value because it's only one frame and the speed difference between 500px/s and 1000px/s isn't very noticeable in a single frame, but if you have a much bigger difference, you can see it better:

 const box = document.querySelector(".box"); const button = document.querySelector(".button"); button.addEventListener("click", () => { // Forcing a huge value here will make the transition // to start from much farther on the right // because it will travel a quite big distance on the first frame // and then start the new transition from the current position // Depending on your monitor's refresh rate you may even see it // coming from the right. box.style.transform = "translateX(500000px)"; box.style.transition = "transform 1s ease-in-out"; requestAnimationFrame(() => { requestAnimationFrame(() => { box.style.transform = "translateX(500px)"; }); }); }); // So we can try multiple times document.querySelector(".reset").onclick = (evt) => { box.style.transform = "none"; box.style.transition = "none"; box.style.offsetWidth; }
 .box { width: 50px; height: 50px; border: 1px solid; }
 <button class="button">click me</button> <button class="reset">reset</button> <div class="box"></div>

So once again I'll play the "hacky" advocate here. Using the synchronous way, you do have control over what happens. Just be very careful that you don't pollute the CSS layout anymore after you've forced that reflow and you'll be fine, the browser won't recalculate it in the rendering step.


As far as I can remember this has always been the expected and actual behavior, I highly doubt this has changed since this talk and you can even retrieve some comments from 2019 under the video that talk about this not working as explained in the video.

To get the expected behavior, you'd need to set the transition along with the setting of the destination value, after the reflow that calculated the new initial position.

 const box = document.querySelector(".box"); const button = document.querySelector("button"); button.addEventListener("click", () => { box.style.transform = "translateX(1000px)"; getComputedStyle(box).transform; box.style.transform = "translateX(500px)"; box.style.transition = "transform 1s ease-in-out"; }); // So we can try multiple times document.querySelector(".reset").onclick = (evt) => { box.style.transform = "none"; box.style.transition = "none"; box.style.offsetWidth; }
 .box { width: 50px; height: 50px; border: 1px solid; }
 <button class="button">click me</button> <button class="reset">reset</button> <div class="box"></div>

or

 const box = document.querySelector(".box"); const button = document.querySelector("button"); button.addEventListener("click", () => { box.style.transform = "translateX(1000px)"; requestAnimationFrame(() => { requestAnimationFrame(() => { box.style.transform = "translateX(500px)"; box.style.transition = "transform 1s ease-in-out"; }); }); }); // So we can try multiple times document.querySelector(".reset").onclick = (evt) => { box.style.transform = "none"; box.style.transition = "none"; box.style.offsetWidth; }
 .box { width: 50px; height: 50px; border: 1px solid; }
 <button class="button">click me</button> <button class="reset">reset</button> <div class="box"></div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM