Vue Principle Interpretation: Do not let transitions/animations become transition components of the dashboard

Animation has always been a tangled point in the front, which is easy to ignore but important. Writing a pleasant and natural interactive experience really adds color to the project. After all, it is easy to feel, so it is necessary to explore the implementation principle of the transition component of vue.The animation implementation of the transition component is divided into two types, using the Css class name and JavaScript hooks, which are described in turn.

Introduction to transition components

This is an abstract component, that is, after rendering the component, it will not appear as any Dom, but will control the internal child nodes in the form of slots.Its purpose is to add/delete Css class names or execute JavaScript hooks at the right time for animation purposes.

transition to VNode

Since it's a component, when you generate a real Dom, you first need to convert it to VNode, then you can take this VNode and convert it to a real Dom.So let's first look at what kind of VNode the transition component will become.

export const transitionProps = { // props properties accepted by transition components
  appear: Boolean, // Whether to render for the first time
  css: Boolean, // Whether to cancel css animation
  mode: String,  // in-out or out-in alternatives
  type: String, // Show declaration to listen on animation or transition
  name: String, // Default v
  enterClass: String, // Default `${name}-enter`
  leaveClass: String, // Default `${name}-leave`
  enterToClass: String, // Default `${name}-enter-to`
  leaveToClass: String, // Default `${name}-leave-to`
  enterActiveClass: String, // Default `${name}-enter-active`
  leaveActiveClass: String, // Default `${name}-leave-active`
  appearClass: String, // Enter at first render
  appearActiveClass: String, // Continue at first render
  appearToClass: String, // Leave at first render
  duration: [Number, String, Object] // Animation duration
}

export default {
  name: 'transition',
  props: transitionProps,
  abstract: true, // Marked as abstract component, does not participate in parent-child component build relationships within vue
  
  render(h) { // Write using render function, and finally know what's called h
    let children = this.$slots.default // Get default slot nodes
    if (!children) {
      return
    }
    if (!children.length) {
      return
    }
    if (children.length > 1) {
      ...There can only be one child node in the slot
    }

    const mode = this.mode
    if (mode && mode !== 'in-out' && mode !== 'out-in') {
      ...mode Can only be in-out or out-in
    }

    const child = children[0] // Child Node Corresponds to VNode
    const id = `__transition-${this._uid}-`
    child.key = child.key == null  // Add key attribute for VNode of child node
      ? child.isComment // Comment Nodes
        ? id + 'comment'
        : id + child.tag
      : isPrimitive(child.key) // Original Value
        ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
        : child.key

    (child.data || (child.data = {})).transition = extractTransitionData(this)
    // Core!Assigning props and hook functions to the transition property of a child node represents a VNode rendered by a transition component
    
    return child
  }
}

export function extractTransitionData(comp) { // Mutator
  const data = {}
  const options = comp.$options
  for (const key in options.propsData) { // props received by transition component
    data[key] = comp[key]
  }
  const listeners = options._parentListeners // Hook method registered on transition component
  for (const key in listeners) {
    data[key] = listeners[key]
  }
  return data
}

From the code above, we know that the transition component mainly does two things: first, it adds the key attribute to the VNode that renders the child nodes, then it adds a transition attribute under its data attribute to indicate that this is a VNode rendered by the transition component, which is handled separately in the process of creating the real Dom by path.

Css Class Name Implementation Principle

First, let's focus on the principle of how Css class names are implemented. Now that we have the corresponding VNode, we need to create a real Dom. In the path process, the style, css, attr and other attributes on the Dom are created into separate modules. These modules have their own hook functions, such as create, update, insert functions. Some modules are different, which means that they are in a certain way.Do something for a period of time.Transition is no exception; the created hook is executed first.As we know, transition components are divided into enter and leave states, so first look at the enter state:

export function enter (vnode) { // Parameter is VNode in component slot
  const el = vnode.elm // Corresponds to the real node
  const data = resolveTransition(vnode.data.transition) // Extended Properties
  // data contains incoming props and six extended class properties
  
  if (isUndef(data)) { // If it's not a transition rendered vnode, bye
    return
  }
  
  ...
}

export function resolveTransition (def) { // Extended Properties
  const res = {}
  extend(res, autoCssTransition(def.name || 'v')) // class object extends to empty object res
  extend(res, def) // Extend attributes on def to res objects
  return res
}

const autoCssTransition (name) { // Generate a class object containing six required classes
  return {
    enterClass: `${name}-enter`,
    enterToClass: `${name}-enter-to`,
    enterActiveClass: `${name}-enter-active`,
    leaveClass: `${name}-leave`,
    leaveToClass: `${name}-leave-to`,
    leaveActiveClass: `${name}-leave-active`
  }
})

Execute enter, and continue to extend the transition property by six before using the class name, so let's move on:

export function enter (vnode) { // VNode inside which parameter is component slot
  ...
  
  const { // Deconstruct required parameters
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearActiveClass,
    appearToClass,
    css,
    type
    // ...omit other parameters
  } = data
  
  const isAppear = !context._isMounted || !vnode.isRootInsert 
  // _isMounted indicates whether the component is mounted
  // isRootInsert indicates whether the root node is inserted
  
  if (isAppear && !appear && appear !== '') {    
  //  If appear property is not configured, the first rendering will exit without animation effect
    return
  }
  
  const startClass = isAppear && appearClass // If there is an appear defined and a corresponding appearClass
    ? appearClass     // Execute the defined appearClass
    : enterClass      // Otherwise, execute enterClass
  const activeClass = isAppear && appearActiveClass
    ? appearActiveClass
    : enterActiveClass
  const toClass = isAppear && appearToClass
    ? appearToClass
    : enterToClass
  
  ...
}

The next step is to take out the props and the extended class values for later use.Then there is the appear implementation principle, if there is no mounted and not root node insertion, and there is a defined appear attribute, then use appearClass to execute the enter state function completely once, otherwise no animation will render directly.Next is the core implementation process.

export function enter (vnode) {
  ...

  const expectsCSS = css !== false && !isIE9 // No explicit indication not to perform css animation

  const cb = once(() => { // Define a cb function that executes only once, but not once
    if (expectsCSS) {
      removeTransitionClass(el, toClass) // Remove toClass
      removeTransitionClass(el, activeClass) // Remove ActeClass
    }
  })
  
  if (expectsCSS) {
    addTransitionClass(el, startClass) // Add startClass
    addTransitionClass(el, activeClass) // Add ActeClass
    nextFrame(() => { // Encapsulation of requestAnimationFrame, executed when next frame browser renders callback
      removeTransitionClass(el, startClass) // Remove startClass
      addTransitionClass(el, toClass) // Add toClass
      whenTransitionEnds(el, type, cb) 
      // Execute cb after browser transition end event transitionend or animationend to remove toClass and activeClass
    })
  }
}

First, define a cb function, which is wrapped by the once function. Its function is to let the function inside execute only once. Of course, this cb is only defined and not executed.Next, we synchronously add startClass and activeClass for the current real node, which is known as v-enter and v-enter-active; then remove startClass and add toClass, which is v-enter-to, in the next frame rendered by the browser in the request Animation Frame; and finally execute the whenTransitionEnds method, which listens for the browser's end of animation event, which is tThe ransitionend or animationend event indicates that the animation or transition defined in v-enter-active is over, the above definition cb is executed at the end, and the toClass and activeClass are removed from this function.

It is not difficult to find that the function of enter state actually does the main thing to manage the addition and deletion of three classes: v-enter/v-enter-active/v-enter-to classes. The specific animation is user defined.

Naturally, we can think that the function of leave status is to add and delete the other three class es managed, and then just show the core code of leave:

export function leave (vnode) {
  const cb = once(() => {
    removeTransitionClass(el, leaveToClass) // Remove v-leave-to
    removeTransitionClass(el, leaveActiveClass) // Remove v-leave-active
  })
  
  addTransitionClass(el, leaveClass) // Add v-leave
  addTransitionClass(el, leaveActiveClass) // Add v-leave-active
  nextFrame(() => { // Browser Next Frame Execution
    removeTransitionClass(el, leaveClass) // Remove v-leave
    addTransitionClass(el, leaveToClass) // Add v-leave-to
    whenTransitionEnds(el, type, cb) // Execute the cb function after the event at the end of the animation
  })
}

There are also many boundary cases handled in the source code, such as transition packages being abstract components, leave s not yet executed when executing enters, enters not completed before executing the last enters, etc.Interested people can see the full source implementation by themselves, here only the core implementation principle is analyzed.Next let's see how JavaScript hooks work.

JavaScript hook implementation principle

Once you know how to implement Css class names, it's easy to understand how JavaScript hooks are implemented.Hook implementation is also divided into enters and leave s, and the code is in these two functions, but the Css method was ignored in the previous introduction. Now let's review these two state functions from the perspective of hook implementation.First, look at enter:

export function enter(vnode) {

  if (isDef(el._leaveCb)) { // If _leaveCb does not execute when entering enter, execute immediately
    el._leaveCb.cancelled = true // Tag bit executed for _leaveCb
    el._leaveCb() // cb._leaveCb will become null when executed
  }
  // el._leaveCb is a cb function defined in the leave state and represents a callback function of the leave state
  // See the cb definition of enter below to see how fat it is

  const {
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    duration
    ... Other parameters
  } = data
  
  const userWantsControl = getHookArgumentsLength(enter) // Incoming enter hook
  // If the parameter of the enter function in the hook is greater than 1, a done function is passed in, indicating that the user wants to control it by himself
  // That's why the Don function needs to be called after the animation ends in enter

  const cb = el._enterCb = once(() => { // This defines the el._enterCb function, which is el._leaveCb in leave
    if (cb.cancelled) { // If the cb function of the enter state is not executed in the leave state, the enterCancelled hook is executed
      enterCancelled && enterCancelled(el)
    } else {
      afterEnter && afterEnter(el) // Otherwise normal execution of afterEnter hook
    }
    el._enterCb = null // After execution, el._enterCb is null
    ... ellipsis css Logical correlation
  })
  
  mergeVNodeHook(vnode, 'insert', () => { // Insert the body of the function into the insert hook, which is executed after the module's created in path
    ...
    enter && enter(el, cb) // Execute the enter hook, passing in cb, where CB corresponds to the Don function in the enter hook
  })
  
  beforeEnter && beforeEnter(el)
  
  nextFrame(() => {
    if (!userWantsControl) { // If the user does not want control
     if (duration) { // If a valid transition time parameter is specified
        setTimeout(cb, duration) // Execute cb after setTimeout
      } else {
        whenTransitionEnds(el, type, cb) // Execute after event after browser transition ends
      }
    }
  })
}

The above code is how JavaScript hooks work, so it's important to note the order in which they are executed:

  1. The beforeEnter hook is executed first because this is synchronous, cb is only defined, insert is executed after creation, nextFrame is the next frame of the browser and is asynchronous.
  2. Executes the body of the function inserted into the insert hook, which is also synchronous, but executes the inner enter hook after the creation.
  3. If the user does not want to control the end of the animation, execute the function body in the nextFrame.
  4. If the user wants control, that is, he calls the done function, directly cb function, which normally executes the afterEnter hook inside.

leave status or just post the core code for everyone to compare with enter, the difference is not very big:

export function leave(vnode) {
  const {
    beforeLeave,
    leave,
    afterLeave,
    duration
    ...  Omit other parameters
  } = data
  
  const cb = once(() => {
    afterLeave && afterLeave(el)
    ...
  })
  
  beforeLeave && beforeLeave(el)
  
  nextFrame(() => {
    if (!userWantsControl) { // User does not want control
      if (isValidDuration(duration)) {
        setTimeout(cb, duration)
      } else {
        whenTransitionEnds(el, type, cb)
      }
    }
  })
  
  leave && leave(el, cb) // User wants to control how done is performed here
}

The order in which hooks are executed in the leave state is beforeLeave, leave, afterLeave.

So far, the two implementation principles of transition's built-in components have all been resolved.There are many more boundaries to consider in the source code, which requires a more comprehensive understanding.

The author is a little disappointed after reading this transition principle, it did not make me a master of animation, the most important thing is that Css's knowledge of animation, so we can see the importance of the foundation firmly established!

Finally, end with a topic that an interviewer might ask, because I've been asked.

The interviewer smiles and asks politely:

  • Please explain how the transition component is implemented below?

Go back:

  • The transition component is an abstract component that does not render any Com. It mainly helps us write animations more easily.Animate the internal single child node as a slot. During the rendering phase, a transition attribute is mounted on the virtual Dom of the child node, representing one of its nodes wrapped by the transition component. During the path phase, the transition component internal hook is executed, which is divided into enter and leave states. The wrapped child node is used with v-if or v-show.Switch state.You can use either Css or JavaScript hooks to add and delete class class names in the enter/leave state when using Css. The user only needs to write an animation of the corresponding class names.If a JavaScript hook is used, the specified functions are also executed sequentially, and these functions need to be defined by the user, and the component simply controls the process.

Next: Burning your head in writing...

A compliment or attention is handy to find.

Reference resources:

All-round in-depth analysis of Vue.js source code

Share a library of components written by the author that may be used some day

A Library of vue functionality components that you may be using is continually improving...

Tags: Front-end Javascript Attribute Vue

Posted on Tue, 24 Mar 2020 23:52:33 -0700 by flaab