vuejs / rfcs

RFCs for substantial changes / feature additions to Vue core
4.85k stars 549 forks source link

All Transition and TransitionGroup JavaScript hooks should be async #543

Closed paya-cz closed 10 months ago

paya-cz commented 2 years ago

What problem does this feature solve?

I'm building a component that animates entering elements using TransitionGroup. If there are many new elements added in a quick succession, the previous enter animation might not have finished before the next one starts. Due to the complexity of the animations I am building, when a new animation starts, I need to inspect the state of the existing animations in order to calculate and start new animations for both existing elements and the new element. The old animations will get discarded at this point.

I use Web Animations API and the transform property for animation. These animations run on a browser composer thread, so even if the JavaScript is busy, the animation still renders nicely. The downside is that Web Animations API is asynchronous. If I call animation.pause, it does not immediately pause the animation because it needs to send a message to the other thread and wait for it to pause the animation.

Now with that out of the way, when a new element enters my TransitionGroup, I need to use the onBeforeEnter hook to pause the animation, wait for the pause to actually take effect (remember that pause is async), then capture the state of the paused animation and the state of the container, in order to prepare the new animations (for both the existing elements and the new entering element).

Currently, only onEnter and onLeave have done callback. I basically need to be able to use await within onBeforeEnter and onBeforeLeave, and I need the TransitionGroup to wait inserting the new element into the DOM until my hooks complete.

I tried to work around this by adding display: none to the element in the onBeforeEnter hook, then do all the async stuff I need in onEnter. The problem I had is at that point, the new element is already in the DOM. Even with display: none, I noticed the animation is not completely fluid and when I call pause, it actually backtracks and jumps a little bit back in time. It seems adding a new element to DOM within the same container somehow affects the running animation enough to disrupt the fluidness of the animation running on the composer thread. I tried to wait using requestAnimationFrame, setTimeout, nextTick tbefore calling the pause, but it didn't help unless I waited for hard-coded 50+ ms. The shorter time I waited, the more common the animation time-travel was. I suppose this is a quirk of the browser animation implementation, yet it could be perfectly fixed if I could just pause the animation in onBeforeEnter, wait for the pause to complete, do the math I need, and only THEN allow the insertion of the element in the DOM. I could also avoid adding display: none to the new element, which might potentially overwrite whatever inline display style the element had.

What does the proposed API look like?

I have several API proposals:

  1. Add new AsyncTransition and AsyncTransitionGroup, where all JavaScript hooks would return a Promise which the transition component would await

  2. Allow all existing hooks to return a Promise. If they do, await it and don't continue until the Promise resolves. For enter and leave, this would also make the done callback redundant.

  3. Add new hooks, such as onBeforeEnterAsync, onEnterAsync, onAfterLeaveAsync, etc. Again they just return a Promise

Whichever solution is chosen, this should affect ALL hooks, including onEnterCancelled etc.