戴兜

戴兜的小屋

Coding the world.
github
bilibili
twitter

!important causes TransitionGroup to fail.

If you have ever used Vue, you may have some understanding of its built-in component TransitionGroup. This article records some usage details of the "movement animation" in TransitionGroup.

Perhaps you are not very familiar with the "movement animation" of TransitionGroup, so let me briefly introduce it here. When used normally, you need to provide a transition style for the .[name]-move class, such as transition: all 0.5s ease;. In this way, when the position of the elements inside TransitionGroup changes, Vue will try to smoothly transition the elements that have changed position from the old position to the new position. Of course, Vue also supports transition effects for adding and removing elements. You just need to provide styles for the [name]-enter-from and [name]-leave-to class names. This is not the focus of this article, so I won't go into detail.

I used to write the styles step by step like most people, and there was no problem.

Until someone in the group told me, "Try adding a persistent transition style with !important to the elements, it will make the transition ineffective."

I was stunned on the spot 😧, which was difficult for me to understand at the time: Vue itself adds the transition property to the elements through [name]-move during the transition, so why does adding a transition property with the highest priority in advance make the transition ineffective?

Starting from the source code#

We can read the code related to TransitionGroup in TransitionGroup.ts.

Initialization phase#

It is not difficult to find that in the render function, Vue assigns the child element array children to the variable prevChildren in the scope of the setup function (L113). At the same time, it traverses prevChildren through a for loop (L135) and stores the position information of each child element in the positionMap (old position).

In this phase, the two relevant contents related to our discussion are here. The assignment of prevChildren allows Vue to obtain the child element references in the subsequent updated lifecycle, making it convenient to perform related operations. And positionMap allows Vue to obtain the original position of the elements in the subsequent operations.

The positionMap here is a WeakMap, and Vue uses the element object as the key value, which ensures that when the element is destroyed, the position information of the corresponding element in positionMap is timely and automatically recycled.

Updated lifecycle#

When the child elements in TransitionGroup change, the callback function registered by onUpdated will be called, and it is also here that Vue completes most of the operations required for the transition.

First of all, Vue traverses again through a forEach to obtain the position information of each element and stores it in newPositionMap (L72). At this time, the new and old positions of an element are stored in newPositionMap and positionMap respectively. What we need to do is to smoothly transition the element from the old position to the new position.

Next is the key point: since it is the updated lifecycle, the elements should already be in the new position, so why transition? Therefore, what we need to do is not simply to transition the elements from the old position to the new position. Instead, we need to put the elements that are already in the new position back to the old position, and then let them smoothly return to the new position to complete the entire transition process. So how does Vue accomplish this?

The code at L73 is like this:

const movedChildren = prevChildren.filter(applyTranslation) 

Vue uses the applyTranslation method to filter out the array of child elements that need to be moved, and by observing the function and variable names, we can almost certainly say that Vue also performs some operations on the elements while filtering, and in fact it does.

function applyTranslation(c: VNode): VNode | undefined {
  const oldPos = positionMap.get(c)!
  const newPos = newPositionMap.get(c)!
  const dx = oldPos.left - newPos.left
  const dy = oldPos.top - newPos.top
  if (dx || dy) {
    const s = (c.el as HTMLElement).style
    s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
    s.transitionDuration = '0s'
    return c
  }
}

For the elements that need to be moved, Vue calculates the difference between the new and old positions, and uses the css property Transform to put the elements back to the old position. Pay special attention to s.transitionDuration = '0s', ~we'll talk about it later~.

But in fact, at this point, the elements have not yet returned to the old position. The browser will add style changes to the rendering queue instead of rendering them immediately. This involves knowledge related to browser reflow, you can search for related articles to read.

In order to ensure that the elements are placed in the old position immediately, after obtaining movedPosition at L73, Vue executes the forceReflow method (L76) to force a reflow. The content of the forceReflow method is also very simple (Transition.ts#L461):

function forceReflow() {
  return document.body.offsetHeight
}

This is also a little frontend knowledge 🤯, reading the offsetHeight or offsetWidth of the document can also trigger a document reflow. You can understand it like this: if there are changes in the rendering queue and you directly get the document width or height without triggering a reflow, you will get outdated element dimensions. Therefore, the browser performs a reflow on the document before reading it.

After that, the work is simple. Just add the [name]-move class to the elements (L81), and then remove the previously added transitionDuration and Transform properties. The elements will naturally return to the new position smoothly~ Listen for the transitionend event (L83-L93) and do some finishing work (remove transition-related class names, etc.).

By reading up to this point, we can already solve the problem mentioned at the beginning of the article. To achieve the transition effect, it is necessary to ensure that the elements are in the old position. In Vue, in order to ensure that the elements are placed in the old position through Transform after the document reflow, Vue sets the transition time of the elements to 0s and performs a forced reflow. However, the manually added high-priority transition property prevents the elements from returning to the old position immediately during the reflow, resulting in no transition effect.

I also created a small demo that simplifies the unrelated code in TransitionGroup. If you are interested, you can take a look at https://codepen.io/DaiDR/pen/VwdMRxa.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.