This article is the fourth in a series on VUE in detail, which is supposedly a separate, larger chapter from the previous article on VUe-Transition in Detail. However goose, the length of the article is too long, so have to carry it out to write a separate. Those interested in previous articles in this series can click on the link below to send them in

  • Vue Core – VDOM
  • In Detail on Vue-Slot
  • In Detail on Vue-Transition

The
component supports the input => output of props and vnode hooks, which package the transition effects for the Enter and leave phases of a single element. This allows us to focus only on the business implementation of CSS and JS hook functions.

In the real world, however, many elements needed to be displayed using transition effects, and it became clear that the
component was not meeting my business needs. At this point, vUE has a built-in
component to meet our needs, which helps us to achieve the transition effect of the list.

First, take an example

As usual, let’s start with an official example

<template>
  <div id="list-demo">
    <button v-on:click="add">Add</button>
    <button v-on:click="remove">Remove</button>
    <transition-group name="list" tag="p">
      <span v-for="item in items" v-bind:key="item" class="list-item">
        {{ item }}
      </span>
    </transition-group>
  </div>
</template>

<script>
export default {
  name: 'home',
  data () {
    return {
      items: [1.2.3.4.5.6.7.8.9].nextNum: 10}},methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
      this.items.splice(this.randomIndex(), 0.this.nextNum++)
    },
    remove: function () {
      this.items.splice(this.randomIndex(), 1)}}}</script>

<style lang="scss">
.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active..list-leave-active {
  transition: all 1s;
}
.list-enter..list-leave-to {
  opacity: 0;
  transform: translateY(30px);
}
</style>
Copy the code

The effect is shown below.

Next, I’ll take you through the design of the
component

Transition-group implementation

In contrast to the
component, the
component is abstract and applies only to a single element. The
component makes the list transition, and it renders a real element node.

However, their design concept is the same. They also provide a props and a series of hook functions for us to use as the interface for input => output conversion or binding

export default {
  props,
  beforeMount () {
    // ...
  },
  render (h: Function) {
    // ...
  },
  updated () {
    // ...
  },
  methods: {
    // ...}}Copy the code

1. Props & other import

props is the same as
props, except that the tag and moveClass attributes are added, and the mode attribute is removed

// props
import { transitionProps, extractTransitionData } from './transition'
const props = extend({
  tag: String.moveClass: String
}, transitionProps)

delete props.mode

// other import
import { warn, extend } from 'core/util/index'
import { addClass, removeClass } from '.. /class-util'
import { setActiveInstance } from 'core/instance/lifecycle'

import {
  hasTransition,
  getTransitionInfo,
  transitionEndEvent,
  addTransitionClass,
  removeTransitionClass
} from '.. /transition-util'
Copy the code

2, render

First, we need to define a set of variables to facilitate subsequent operations

  • tag: We can see from the overall context of the design above,<transition-group>noabstractProperty that it will render a real node, then nodetagIs required, and its default value isspan.
  • map: Creates an empty object
  • prevChildren: is used to store the last child node
  • rawChildren: get<transition-group>The child node of the package
  • children: stores the current child node
  • transitionData: Gets render data on the component
const tag: string = this.tag || this.$vnode.data.tag || 'span'
const map: Object = Object.create(null)
const prevChildren: Array<VNode> = this.prevChildren = this.children
const rawChildren: Array<VNode> = this.$slots.default || []
const children: Array<VNode> = this.children = []
const transitionData: Object = extractTransitionData(this)
Copy the code

This is followed by node traversal, where the binding of the transition animation is performed on each node in the list

  • rightrawChildrenGo through and place eachvnodeNode extraction;
  • If the node contains any__vlistThe characterkey, it willvnodePut it in thechildren;
  • Then the transition data will be extractedtransitionDataAdded to thevnode.data.transitionTo animate the transitions of individual elements in the list
for (let i = 0; i < rawChildren.length; i++) {
  const c: VNode = rawChildren[i]
  if (c.tag) {
    if(c.key ! =null && String(c.key).indexOf('__vlist')! = =0) { children.push(c) map[c.key] = c ; (c.data || (c.data = {})).transition = transitionData }else if(process.env.NODE_ENV ! = ='production') {
      constopts: ? VNodeComponentOptions = c.componentOptionsconst name: string = opts ? (opts.Ctor.options.name || opts.tag || ' ') : c.tag
      warn(`<transition-group> children must be keyed: <${name}> `)}}}Copy the code

PrevChildren is then processed

  • ifprevChildrenIf it exists, it is traversed and willtransitionDataAssigned tovnode.data.transitionAfter that, whenvnodeChild nodesenterleaveIf there is a transition animation, the corresponding transition animation will be executed
  • Call nativegetBoundingClientRectGets the element’s position information and records it tovnode.data.pos
  • And then determinemapExists invnode.key, if exists, willvnodeIn thekeptMedium, otherwise throw toremovedIn the queue
  • Finally, place the rendered elements inthis.kept,this.removedIs used to record the nodes removed
if (prevChildren) {
  const kept: Array<VNode> = []
  const removed: Array<VNode> = []
  for (let i = 0; i < prevChildren.length; i++) {
    const c: VNode = prevChildren[i]
    c.data.transition = transitionData
    c.data.pos = c.elm.getBoundingClientRect()
    if (map[c.key]) {
      kept.push(c)
    } else {
      removed.push(c)
    }
  }
  this.kept = h(tag, null, kept)
  this.removed = removed
}
Copy the code

Finally,
renders

return h(tag, null, children)
Copy the code

Update & methods

Now that we’ve tied the transition to the render phase for each element in the list, we’re going to update the list dynamically as each element changes. So how does that work? So let’s get this logic straight

I. Check whether you need to move

  • First of all inupdateIn the hook function, we get the last child nodeprevChildrenmoveClass; Then judgechildrenDoes it exist andchildrenWhether or nothas moveIf,childrenDoes not exist, orchildrenThere is nomoveState, then there is no need to continueupdatemoveTransition, directreturnCan be
const children: Array<VNode> = this.prevChildren
const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
if(! children.length || !this.hasMove(children[0].elm, moveClass)) {
  return
}
Copy the code
  • hasMove(): This method is mainly used to judgeelWhether the node hasmoveIn the state.
  • The current settingreturnIf not, it clones a DOM node and then removes all of the elements to avoid CSS transitions inside the elementtransitionClasses
  • Next, add the clone node againmoveClassAnd should bedisplaySet tononeAnd then add tothis.$el
  • And then go throughgetTransitionInfoTo get ittransitionRelevant information and then fromthis.$elRemove it. At this point we’ve already gotten whether the node hastransformThe information of the
export consthasTransition = inBrowser && ! isIE9 hasMove (el: any,moveClass: string): boolean {
  // If transition is not in your browser, or the browser does not support transition, return false
  if(! hasTransition) {return false
  }
  // If the current instance context has _hasMove, return the value of _hasMove
  if (this._hasMove) {
    return this._hasMove
  }
  const clone: HTMLElement = el.cloneNode()
  if (el._transitionClasses) {
    el._transitionClasses.forEach((cls: string) = > { removeClass(clone, cls) })
  }
  addClass(clone, moveClass)
  clone.style.display = 'none'
  this.$el.appendChild(clone)
  const info: Object = getTransitionInfo(clone)
  this.$el.removeChild(clone)
  return (this._hasMove = info.hasTransform)
}
Copy the code

Ii. Move Transition implementation

  • Then, a wave of pre-processing is carried out for the child nodes. Here, three cycles are used for the processing of the child nodes, mainly to avoid the chaos of reading and writing DOM in each cycle and to help prevent the layout disorder
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
Copy the code

The three functions are treated as follows

  1. callPendingCbs(): Judge whether the transition animation of the previous frame of each node is completed. If not, it will be executed in advance_moveCb()_enterCb()
  2. recordPosition(): Records the new location of each node
  3. applyTranslation(): Obtain the old and new node positions respectively, and calculate the difference. If there is a difference, set the node’stransformProperty offsets the node to be moved to its previous position, which is the listmoveTo prepare for
function callPendingCbs (c: VNode) {
  if (c.elm._moveCb) {
    c.elm._moveCb()
  }
  if (c.elm._enterCb) {
    c.elm._enterCb()
  }
}

function recordPosition (c: VNode) {
  c.data.newPos = c.elm.getBoundingClientRect()
}

function applyTranslation (c: VNode) {
  const oldPos = c.data.pos
  const newPos = c.data.newPos
  const dx = oldPos.left - newPos.left
  const dy = oldPos.top - newPos.top
  if (dx || dy) {
    c.data.moved = true
    const s = c.elm.style
    s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
    s.transitionDuration = '0s'}}Copy the code
  • Next, a traversal implementation is performed on the child elementsmoveTransition. It passes the fetch before iteratingdocument.body.offsetHeight, which causes the calculation to occur, triggering backflow and allowing the browser to redraw
  • And then you start rightchildrenTraversal, period ifvnode.data.movedtrue, the implementationaddTransitionClassPlus the child nodemoveClassAnd should bestyle.transformProperty clearing, since we have shifted the child node to the previous old position in the child node preprocessing, it will transition from the old position to the current position, that is what we wantmoveTransitional effect
  • And then it adds to the nodetransitionendTransition to the end of the listening event, in the event to do some cleaning operations
this._reflow = document.body.offsetHeight

children.forEach((c: VNode) = > {
  if (c.data.moved) {
    const el: any = c.elm
    const s: any = el.style
    addTransitionClass(el, moveClass)
    s.transform = s.WebkitTransform = s.transitionDuration = ' '
    el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
      if(e && e.target ! == el) {return
      }
      if(! e ||/transform$/.test(e.propertyName)) {
        el.removeEventListener(transitionEndEvent, cb)
        el._moveCb = null
        removeTransitionClass(el, moveClass)
      }
    })
  }
})
Copy the code

Note: I’ll summarize the browser backflow triggers, such as browser window changes, computing styles, adding or removing elements to the DOM, changing element classes, and so on

  1. Add or remove visible DOM elements
  2. Element position change
  3. Element size changes — margins, padding, borders, width, and height
  4. Content changes, such as changes in the width and height of calculated values caused by changes in the size of text, text, or images entered by the user in the input field
  5. Page render initialization
  6. Browser window size changes – when the resize event occurs
  7. Calculate the offsetWidth and offsetHeight properties
  8. Sets the value of the style property

4, beforeMount

Since VDOM does not guarantee a relative position of the removed element when the node diff is updated. So we need to override the update rendering logic in the beforeMount hook function to achieve the desired effect

  • Get the instance itself firstupdateMethod to cache
  • We know that from up herethis.keptIs the last node cached, and the number of nodes in it has been increasedtransitionTransition properties. So let’s go throughsetActiveInstanceCache the current instance, and thenvnodefor__patch__Operation to remove what needs to be removedvnodeAnd then executerestoreActiveInstancePoint the example to recovery
  • thenthis.keptAssigned tothis._vnodeTo trigger the transition
  • Last to perform cacheupdateRendering node
beforeMount () {
  const update = this._update
  this._update = (vnode, hydrating) = > {
    const restoreActiveInstance = setActiveInstance(this)
    // force removing pass
    this.__patch__(
      this._vnode,
      this.kept,
      false.// hydrating
      true // removeOnly (! important, avoids unnecessary moves)
    )
    this._vnode = this.kept
    restoreActiveInstance()
    update.call(this, vnode, hydrating)
  }
}
Copy the code
  • setActiveInstance
export let activeInstance: any = null
export function setActiveInstance(vm: Component) {
  const prevActiveInstance = activeInstance
  activeInstance = vm
  return () = > {
    activeInstance = prevActiveInstance
  }
}
Copy the code

The last

That’s about it, and we’re done parsing the transition related built-in components
and
. Different component types, one abstract and one render the actual node elements, want to do the same thing: initialize the input interface to the user, and then get the transition effect of the output.

Front end communication group: 731175396, warmly welcome all girls, Hanzi enthusiastically join