preface

As a front-end dish chicken, there may be many wrong places in my understanding, I hope you correct me. This article is the result of my meditation these days, and I can’t guarantee that what I say is completely right. It can also be said as a brick to attract jade, and I look forward to your correction.

This article is transferred from the author’s personal technology blog: Wood ling Fish

A brief introduction to Render

The source code for Ele. me’s foldanimation is located in the: SRC/Transitions directory, gayhub: link

Because the content is not much, I directly posted the source code:

import { addClass, removeClass } from 'element-ui/src/utils/dom';

class Transition {
  beforeEnter(el) {
    addClass(el, 'collapse-transition');
    if(! el.dataset) el.dataset = {}; el.dataset.oldPaddingTop = el.style.paddingTop; el.dataset.oldPaddingBottom = el.style.paddingBottom; el.style.height ='0';
    el.style.paddingTop = 0;
    el.style.paddingBottom = 0;
  }

  enter(el) {
    el.dataset.oldOverflow = el.style.overflow;
    if(el.scrollHeight ! = =0) {
      el.style.height = el.scrollHeight + 'px';
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    } else {
      el.style.height = ' ';
      el.style.paddingTop = el.dataset.oldPaddingTop;
      el.style.paddingBottom = el.dataset.oldPaddingBottom;
    }

    el.style.overflow = 'hidden';
  }

  afterEnter(el) {
    // for safari: remove class then reset height is necessary
    removeClass(el, 'collapse-transition');
    el.style.height = ' ';
    el.style.overflow = el.dataset.oldOverflow;
  }

  beforeLeave(el) {
    if(! el.dataset) el.dataset = {}; el.dataset.oldPaddingTop = el.style.paddingTop; el.dataset.oldPaddingBottom = el.style.paddingBottom; el.dataset.oldOverflow = el.style.overflow; el.style.height = el.scrollHeight +'px';
    el.style.overflow = 'hidden';
  }

  leave(el) {
    if(el.scrollHeight ! = =0) {
      // for safari: add class after set height, or it will jump to zero height suddenly, weired
      addClass(el, 'collapse-transition');
      el.style.height = 0;
      el.style.paddingTop = 0;
      el.style.paddingBottom = 0; }}afterLeave(el) {
    removeClass(el, 'collapse-transition');
    el.style.height = ' '; el.style.overflow = el.dataset.oldOverflow; el.style.paddingTop = el.dataset.oldPaddingTop; el.style.paddingBottom = el.dataset.oldPaddingBottom; }}export default {
  name: 'ElCollapseTransition'.functional: true.render(h, { children }) {
    const data = {
      on: new Transition()
    };

    return h('transition', data, children); }};Copy the code

Leaving the Transition class aside, this js file creates a Transition element using render.

The Render function takes two arguments, the first being h, the method used to create the VNode element (createElement), which is the generic and officially recommended abbreviation. The second is the Context object, which is used to obtain data for the current component, such as props, data, slots, and a number of other data objects.

Context A context must be declared as a functional component

Context:

  • props: Provides objects for all prop
  • children: An array of VNode children
  • slots: a function that returns an object containing all slots
  • scopedSlots(2.6.0+) an object that exposes the incoming scope slot. Normal slots are also exposed as functions.
  • data: passes to the entire componentThe data objectAs acreateElementThe second parameter is passed to the component
  • parent: a reference to the parent component
  • listeners(2.3.0+) An object containing all event listeners registered by the parent component for the current component. This is adata.onAn alias for “.
  • injections(2.3.0+) If usedinjectOption, the object contains the property that should be injected.

In ele. me transtion, children are the elements that you write inside when you use it.

Ex. :

<template>
  <div>
    <el-collapse-transition>
      <! -- I am a child -->
      <div v-if="show">xxxx</div>  
    </el-collapse-transition>
  </div>
<template>
<script>
import ElCollapseTransition from 'element-ui/src/transitions/collapse-transition';
export default {
  data(){
    return {
      show: false,}},components: { 
    ElCollapseTransition 
  },
}
</script>
Copy the code

With children, you can dispense with traditional vue files and use slots to receive content (without writing vue files, and therefore templates).

Arguments to the h function

  1. An HTML tag name, component option object, or an async function that resolves any of the above. Required fields. {String | Object | Function}
  2. A data object corresponding to an attribute in a template. Optional. {String | Array}
  3. Child virtual nodes (VNodes)createElement()You can also use strings to generate “text virtual nodes”. Optional.

CreateElement parameter

The first parameter is a tag name, and the second parameter is an attribute, such as the click event binding, the style, class, and props Settings. The third parameter is a subset node, children

The binding of events, in render, takes the form of an ON object, as described in the official documentation: Event & keystroke modifiers

on: {
  'click': this.doThisInCapturingMode,
  'keyup': this.doThisOnce,
  'mouseover': this.doThisOnceInCapturingMode
}
Copy the code

Since the Ele. me folding animation uses a JS animation hook and is a custom event, our on object should end up like this:

on: {
  'beforeEnter': function(){},
  'enter': function(){},
  'afterEnter': function(){},
  'beforeLeave': function(){},
  'leave': function(){},
  'afterLeave': function(){},}Copy the code

Merge the

export default {
  name: 'ElCollapseTransition'.functional: true.render(h, { children }) {
    const data = {
      on: {
        'beforeEnter': function(){},
        'enter': function(){},
        'afterEnter': function(){},
        'beforeLeave': function(){},
        'leave': function(){},
        'afterLeave': function(){},}};return h('transition', data, children); }};Copy the code

The role of functional component declarations

The official recommendation does not manage any state, does not listen for any state passed to it, and has no lifecycle methods, just a function that accepts some prop. Functional functional components are recommended!

Our Transition doesn’t handle any states of its own, and it doesn’t have a life cycle, just a prop that accepts custom event functions.

More importantly, declare functional, and the h function will have a second upper and lower file object argument. In order to get children.

Of course, you can get it using slots, but children gets all, and slots has a named attribute, so if you use named, you might get incomplete children.

Using slot

Do not declare functional components

export default {
  name: 'ElCollapseTransition'.render(h) {
    const data = {
      on: {
        'beforeEnter': function(){},
        'enter': function(){},
        'afterEnter': function(){},
        'beforeLeave': function(){},
        'leave': function(){},
        'afterLeave': function(){},}};return h('transition', data, this.$slots.default); }};Copy the code

This is also possible, but I personally recommend the functional formula because it’s faster (in a sense).


To this, the understanding of render is almost enough, how to create transtion, and child node processing, data binding, why declare functional components, presumably should have a simple understanding.

Bugs generated by using the class class

The for loop is bound to each of the key/value pairs, but with class, there is a problem: the for loop can’t get the property method in the class

Why is that?

Because in class, these beforeEnter methods are declared as properties on Prototype and set to non-enumerable. Prevent the method we set from being retrieved by the for loop.

So, ele. me should have a problem with this:

 const data = {
     on: new Transition()
 };
Copy the code

Although it returns an object, the property methods on that object cannot be looped out by for, and on cannot bind properly.

To verify my guess, I manually manipulated a few attributes and found that the ON binding was in effect, with the following code:

export default {
  name: 'ElCollapseTransition'.functional: true.render(h, { children }) {
    const t = new Transition();
    
    console.log(Object.getOwnPropertyDescriptor(t.__proto__, "beforeEnter"));  
    //{writable: true, Enumerable: false, different: true, value: ƒ}

    // Manually set several properties to be enumerable
    Object.defineProperties(t.__proto__, {
      beforeEnter: {
        enumerable: true
      },
      enter: {
        enumerable: true
      },
      afterEnter: {
        enumerable: true}})const data = {
      on: t
    };

    return h('transition', data, children); }};Copy the code

Deal with this bug

We can either change the class to a normal object, or manually set the enumerable object for each property to true.

Animation – JS hook understanding

Js animation has 8 hooks, which can also be understood as the animation’s life cycle:

  1. BeforeEnter Enters – before animation starts
  2. Enter enter – Animation starts
  3. AfterEnter Enter – Animation ends
  4. EnterCancelled – Animation cancelled
  5. BeforeLeave Leave – Before the animation starts
  6. Leave – Animation begins
  7. AfterLeave – End of animation
  8. The leaveCancelled leave animation was cancelled

The leaveCancelled hook will only trigger if a V-show is used.

EnterCancelled Hook triggers the exit animation before the animation has finished while the animation is switching fast.

The usual type of hook should be removed as leaveCancelled and enterCancelled, and the remaining hook can be used.

One thing to note is that if the animation toggle too fast, the end of the animation life cycle will not trigger: afterEnter, afterLeave

So be sure to initialize the style before the animation starts. For example, if I want to animate the height of an element from 0 to its actual height, always set the width to 0 and start with the actual height at beforeEnter, or unset 0. So there’s going to be a transition from zero to something.

Hook parameters

For all hooks, the first argument is the dom element that triggers the animation, called EL.

Enter and leave have the second argument done. Done is a callback function. If you write this parameter on the function, vue discontinues the default animation event listener.

Done gives you more room to play, sort of like the Promise callback, which will only end if it fires, otherwise the outside will continue to wait.

const Transition = {
  beforeEnter(el) {},

  enter(el, done) {},

  afterEnter(el) {},

  beforeLeave(el) {},

  leave(el, done) {},

  afterLeave(el){},}Copy the code

If you do not use done, vue will sniff for transition or animation in your CSS and then listen for the corresponding end-of-animation event:

  1. transitionend
  2. animationend

Run done in the event callback to go to the next animation hook.

However, in some scenarios, you may need to have two transition effects for the same element at the same time. For example, the animation is triggered quickly and completed, and the transition effect is not finished. In this case, you need to use the Type attribute and set the animation or transition to explicitly declare the type you want Vue to listen to.

Apparently, ele. me doesn’t use “done” for its folding animation. I say “done” so we can verify some guesses later.

A profound

Now that we know what the hook means, we can start by manually writing our own animation, such as a width animation.

According to the component

<template>
  <div>
    <button @click="show=! show">Show hidden</button>
    <MuWidthTransition>
      <div v-show="show" class="box">xxx</div>
    </MuWidthTransition>
  <div>
</template>
<script>
import MuWidthTransition from "@/components/default/transitions/width-transition";
export default {
  data() {
    return {
      show: false}},components: {
    MuWidthTransition 
  }
}
</script>
<style lang="scss" scoped>
.box {
  width: 200px;
  height: 200px;
  background-color: red;
}

.mu-width-transition {
  transition: width 0.25 s;
}
</style>
Copy the code

The animation component

const Transition = {
  beforeEnter(el) {
    el.classList.add("mu-width-transition");
    el.style.width = "0";
    el.style.overflow = 'hidden';
  },

  enter(el) {
    el.style.width = "";
  },

  afterEnter(el) {
    el.classList.remove('mu-width-transition');

    el.style.width = "";
    el.style.overflow = "";
  },

  beforeLeave(el) {
    el.classList.add("mu-width-transition");
    el.style.width = "";
    el.style.overflow = "hidden";
  },

  leave(el) {
    el.style.width = "0";
  },

  afterLeave(el) {
    el.style.width = "";
    el.style.overflow = ' ';
    el.classList.remove('mu-width-transition'); }},export default {
  name: 'MuWidthTransition'.functional: true.render(h, { children }) {
    const data = {
      on: Transition
    };

    return h('transition', data, children); }};Copy the code

Our logic is simple:

  • Set element width to 0 before animation begins, overflow: “hidden”; Add animation transition class: mu-width-transition
  • When the animation starts, the width is removed and the element width is restored
  • Delete the transition class at the end of the animation, overflow empties
  • Before leaving add transition class, width empty, overflow: “hidden”;
  • It leaves with a width of 0 and the element starts to shrink
  • Leaving the end, element width is cleared, overflow is cleared, and transition classes are deleted

On paper, we’re on the right track.

However, you get a result: no animation on entry, animation on exit, and animation on exit when you click enter

Way???

A test run. – Why it didn’t work

If you think back to the days when you were writing CSS animations, it was a good idea to set display and wait a little while before turning display: None into a block, to set CSS styles or add classes. Elements are animated, is that the question?

We change:

enter(el, done) {
    setTimeout(() = > {
        el.style.width = "";
        setTimeout(() = > {
            done();
        }, 250)},20)},Copy the code

With setTimeout we delay triggering the width setting for 20ms and then wait for the animation to finish triggering done.

The effect does work:

Do you think, sure enough, but, “always” is right?

Why do we need to delay when hungry people don’t? What’s so magical about it?

Why do we need a delay?

Remember, CSS animation is done frame by frame, and the interval between each frame is about 17ms, but it can vary from browser to browser, it can be shorter.

And JS code is fast, the completion of multiple lines of code may not need 1ms, or even 0.xxxms.

However, the element was added CSS style instantly because of too fast, and the frame rate of the animation did not have time to respond, which led to the failure of our animation. The theoretical knowledge was correct, but it was too fast.

For example: The little guy died as soon as he was born. He didn’t even have time to cry!

A deeper understanding, my personal thoughts:

When the element is rendered in this frame is not triggered, width becomes 0, then in an instant and clear width, and the frame of the rendering will be in accordance with the style of the final results apply colours to a drawing, the width of the element set at this time and not set before, so there will be no animation, return how animation of a MAO, direct display.

Will there be animation when shrinking?

The width is already there, we just let it shrink, it doesn’t affect the animation frame, normal render.

Why does hungry me animation not need delay?

Do not know hungry me intentionally or unintentionally, under a fairy hand, this move let the whole plate chess are alive. But before we get to that, let’s take a look at: browser backflow and redraw

See this article from Nuggets: Reflow & Repaint for browsers

Js also allows the browser to generate backflow:

  • clientWidth,clientHeight,clientTop,clientLeft
  • offsetWidth,offsetHeight,offsetTop,offsetLeft
  • scrollWidth,scrollHeight,scrollTop,scrollLeft
  • scrollIntoView(),scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

** Backflow triggers a redraw, which causes the browser to clear the current element’s calculation queue and recalculate. ** In this way, we can get accurate results.

Looking back at the ele. me code, there is the word el.scrollHeight, which obviously triggers backflow in the browser.

So what does the backflow do here?

Backflow recalculates the style of the element, including the style you added before the animation started, height=”0″; And because of the recalcalculation, there will be some delay on the synchronization thread, and the last frame of this element will end up with a height of 0, and the next frame, we js set the height to the specified height or empty, which will result in two different results, which are different from the previous ones, and the animation will be generated.

Try big wheel

Now that we know why, it’s time to move in

The animation component

const Transition = {
  beforeEnter(el) {
    el.classList.add("mu-width-transition");
    el.style.width = "0";
    el.style.overflow = 'hidden';
  },

  enter(el) {
    el.offsetWidth;
    el.style.width = "";
  },

  afterEnter(el) {
    el.classList.remove('mu-width-transition');

    el.style.width = "";
    el.style.overflow = "";
  },

  beforeLeave(el) {
    el.classList.add("mu-width-transition");
    el.style.width = "";
    el.style.overflow = "hidden";
  },

  leave(el) {
    el.style.width = "0";
  },

  afterLeave(el) {
    el.style.width = "";
    el.style.overflow = ' ';
    el.classList.remove('mu-width-transition'); }},export default {
  name: 'MuWidthTransition'.functional: true.render(h, { children }) {
    const data = {
      on: Transition
    };

    return h('transition', data, children); }};Copy the code

The result is very satisfactory, exactly as we expected.

We also took full advantage of the dangers. After all, backflow and redraw have always been a pit to avoid when writing on the front end, but knowing your enemy can be a friend in a sense. Give us a hand.

Little Dodger (waiting for the big guy’s answer)

One might wonder, since backflow changes the final result of the frame, wouldn’t it be nice to backflow after all the properties are configured before animation?

 beforeEnter(el) {
     el.classList.add("mu-width-transition");
     el.style.width = "0";
     el.style.overflow = 'hidden';
     el.offsetWidth;
 },

 enter(el) {
     el.style.width = "";
 },
Copy the code

BeforeEnter and Enter are obviously not the same task, this should involve the mechanism of js running, I am a food, also not very understand, the analysis of the fact that there is no animation effect. BeforeEnter and Enter are obviously not the same task, this should involve the mechanism of JS running.

BeforeEnter and Enter are not the same task. OffsetWidth is delayed, but it is not running in the same pipe as JS. When offsetWidth recalculates, width becomes “” again, and the animation is gone.

Width = “”, el.style.width = “”, so I will wait for the previous hook to run.

Another reason is that all beforeEnter processing is too fast, so the offsetWidth calculation does not start the frame even after redrawing.

After all, it stands to reason that we can also trigger animations on beforeEnter, as long as we get the initialized data before the frame number and then get a different result on subsequent frames.

 beforeEnter(el) {
     el.classList.add("mu-width-transition");
     el.style.width = "0";
     el.style.overflow = 'hidden';
     el.offsetWidth;
     el.style.width = "";
 },

 enter(el){},Copy the code

This writing also fails to generate animation because it ignores another factor, the animation frame rate time. It doesn’t matter if you generate a new result and MY frame rate doesn’t react.

To gauge this idea, I output the timestamp:

 beforeEnter(el) {
     el.classList.add("mu-width-transition");
     el.style.width = "0";
     el.style.overflow = 'hidden';
     
     console.time("off")
     el.offsetWidth;
     console.timeEnd("off")

     el.style.width = "";
 },

 enter(el){},Copy the code

Time of offsetWidth: 0.136962890625 ms; There is no animation

I used promise+setTimeout+asyns instead for delay processing

 async beforeEnter(el) {
     el.classList.add("mu-width-transition");
     el.style.width = "0";
     el.style.overflow = 'hidden';
     
     console.time("off")
     await new Promise((resolve, reject) = > {
      setTimeout(() = > {
        el.style.width = "200px";
        resolve();
      }, 1)})console.timeEnd("off")

     el.style.width = "";
 },
Copy the code

Await time: 1.535888671875ms; With animation

So let’s figure out how long it took to animate

beforeEnter(el) {
    el.classList.add("mu-width-transition");
    el.style.width = "0";
    el.style.overflow = 'hidden';

    console.time("off")},enter(el) {
    el.offsetWidth;
    console.timeEnd("off")
    
    el.style.width = "";

    
  },
Copy the code

Time: 0.3779296875 ms

Obviously, backflow at beforeEnter is very fast, too fast, causing the frame rate to not keep up. The following text is slightly slower in time and the animation appears.

conclusion

There are two conditions for animation to occur:

  1. The results should be different before and after
  2. At a reasonable frame rate, not too fast

We can also explain why when the display element changes from None to block, the added CSS changes are not animated, because it is too fast, the previous frame is the same as the subsequent frame, no animation can be generated, and none elements are not rendered, all calculations are done at the same time.

This is probably why, when the state of the element is displayed, the animation can be added to the effect.