Vue2.x 源码 - transition-group

上一篇:Vue2.x 源码 - transition

上一章我们看来一下<transiiton> 组件的实现原理,它只能针对单⼀元素实现过渡效果,Vue 还提供了 <transition-group> 组件来实现多个元素/组件的过渡效果;

<transition-group> 渲染一个真实的 DOM 元素;默认渲染 <span>,可以通过 tag attribute 配置哪个元素应该被渲染;

注意:每个 <transition-group> 的子节点必须有独立的 key,动画才能正常工作;

1、源码实现

src/platforms/web/runtime/components/transitions-group.js 中:

//获取props
const props = extend({
  tag: String,
  moveClass: String
}, transitionProps)
//删除mode
delete props.mode
export default {
  props,
  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)
    }
  },
  render (h: Function) {
    const tag: string = this.tag || this.$vnode.data.tag || 'span'
    const map: Object = Object.create(null)
    //存储上⼀次的⼦节点
    const prevChildren: Array<VNode> = this.prevChildren = this.children
    //表⽰ <transtition-group> 包裹的原始⼦节点
    const rawChildren: Array<VNode> = this.$slots.default || []
    //⽤来存储当前的⼦节点
    const children: Array<VNode> = this.children = []
    //从 <transtition-group> 组件上提取出来的⼀些渲染数据
    const transitionData: Object = extractTransitionData(this)
    //遍历 rawChidren ,初始化 children
    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') {
          const opts: ?VNodeComponentOptions = c.componentOptions
          const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
          warn(`<transition-group> children must be keyed: <${name}>`)
        }
      }
    }
    //处理 prevChildren
    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
    }
    return h(tag, null, children)
  },
  updated () {
    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
    }
    // 我们将工作分成三个循环,以避免在每次迭代中混合读取和写入DOM——这有助于防止布局抖动
    children.forEach(callPendingCbs)
    children.forEach(recordPosition)
    children.forEach(applyTranslation)
    // 强制reflow将所有东西放置到指定的位置,以避免在摇树过程中被移除
    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)
          }
        })
      }
    })
  },
  methods: {
    hasMove (el: any, moveClass: string): boolean {
      if (!hasTransition) {
        return false
      }
      if (this._hasMove) {
        return this._hasMove
      }
      // 检测应用了move类的元素是否有CSS转换。
      //因为此时元素可能在一个正在进入的转换中,所以我们复制了它,并删除了所有其他应用的转换类,以确保只应用了move类。
      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)
    }
  }
}

render
<transition-group> 组件也是由 render 函数渲染⽣成 vnode<transition-group> 组件是⾮抽象组件,它会渲染成⼀个真实元素, 默认 tagspan

1、遍历 <transtition-group> 包裹的原始⼦节点,初始化 children,拿到每一个 vnode,判断 vnode 是否设置了 key,没设置则会报错;设置了把 vnode 添加到 children 中,然后把提取的数据 transitionData 添加的 vnode.data.transition 中,这样 才能实现列表中单个元素的过渡动画;
2、当上一次子节点存在,遍历获取 vnode ,然后把 transitionData 赋值到 vnode.data.transition ,这个是为了当它在 enterleave 的钩⼦函数中有过渡动画;
3、接着⼜调⽤了原⽣ DOM 的 getBoundingClientRect ⽅法获取到原⽣ DOM 的位置信息,记录到 vnode.data.pos 中,然后判 断⼀下 vnode.key 是否在 map 中,如果在则放⼊ kept 中,否则表⽰该节点已被删除,放⼊ removed 中;
4、然后通过执⾏ h(tag, null, kept) 渲染后放⼊ this.kept 中,把 removedthis.removed 保存;
5、最后整个 render 函数通过 h(tag, null, children) ⽣成渲染 vnode

update
在元素插入和删除的时候,会重新执行 render 函数来渲染新的节点,还要触发 update 钩子函数;

1、利用 hasMove 判断⼦元素是否定义 move 相关样式,不存在直接返回;

hasMove 首先克隆一个 DOM 节点,然后为了避免影响而移除它的过渡 class ,接着添加了 moveClass 样式,设置 displaynone ,添加到组件根节点上;接下 来通过 getTransitionInfo 获取它的⼀些缓动相关的信息;然后 从组件根节点上删除这个克隆节点,并通过判断 info.hasTransform 来判断 hasMove

2、子节点预处理,对 children 做了3个循环并分别做了处理;

callPendingCbs ⽅法是在前⼀个过渡动画没执⾏完⼜再次执⾏到该⽅法的时候,会提前执⾏ _moveCb_enterCb
recordPosition 的作⽤是记录节点的新位置;
applyTranslation 的作⽤是先计算节点新位置和旧位置的差值,如果差值不为 0,则说明这些节点 是需要移动的,所以记录 vnode.data.moved 为 true,并且通过设置 transform 把需要移动的节点 的位置⼜偏移到之前的旧位置,⽬的是为了做 move 缓动做准备;

3、遍历子元素实现 move 过渡,⾸先通过 document.body.offsetHeight 强制触发浏览器重绘,接着再次对 children 遍历,先给 ⼦节点添加 moveClass;接 着把⼦节点的 style.transform 设置为空;接下来会监听 transitionEndEvent 过渡结束的事件,做⼀些清理的操作;

beforeMount
由于虚拟 DOM 的⼦元素更新算法是不稳定的,它不能保证被移除元素的相对位置,所以我们强制 <transition-group> 组件更新⼦节点通过 2 个步骤:
1、第⼀步我们移除需要移除的 vnode ,同时触发它们的 leaving 过渡;
2、第⼆步我们需要把插⼊和移动的节点达到它们的最终位置,同时还要保证移除的节点保留在应该的位置,⽽这个是通过 beforeMount 钩⼦函数来实现的

通过把 __patch__ ⽅法的第四个参数 removeOnly 设置为 true,这样在 updateChildren 阶段, 是不会移动 vnode 节点的;

2、props

<transition-group> 组件的 props 跟 <transition> 组件的props 差不多;只是有个别差别;

tag:组件渲染成的真实元素,默认是 span;
moveClass:覆盖移动过渡期间应用的 CSS 类;
name:用于自动生成 CSS 过渡类名;比如 name 为 test ,自动生成:test-enter(过渡开始状态,一帧)、test-enter-to(过渡插入之后和结束之前)、test-enter-active(过渡结束状态,一帧)、test-leave、test-leave-to、test-leave-active等;
appear:是否在初始渲染时使用过渡,默认为 false;
css:是否使用 CSS 过渡类,默认为 true;设置为 false,将只通过组件事件触发注册的 JavaScript 钩子;
type:指定过渡事件类型,侦听过渡何时结束;有效值为 “transition” 和 “animation”;
duration:指定过渡的持续时间,{ enter: number, leave: number } ;
自定义类名:将name默认生成的类名替换成对应阶段自定义的类名;enterClass、leaveClass、enterToClass、leaveToClass、enterActiveClass、leaveActiveClass、appearClass、appearActiveClass、appearToClass;

<transition-group>组件没有 mode 属性,但是比 <transition> 组件多了 tagmoveClass 属性;

3、事件

before-enter:设置过渡进入之前的组件状态;
enter:设置过渡进入完成时的组件状态;
after-enter:设置过渡进入完成之后的组件状态;
enter-cancelled:设置过渡进入取消之后的组件状态;
before-leave:设置过渡离开之前的组件状态;
leave:设置过渡离开完成时的组件状态;
after-leave:设置过渡离开完成之后的组件状态;
leave-cancelled (v-show only):设置过渡离开取消之后的组件状态;
before-appear:设置过渡显示之前的组件状态;
appear:设置过渡显示完成时的组件状态;
after-appear:设置过渡显示完成之后的组件状态;
appear-cancelled:设置过渡显示取消之后的组件状态;

事件跟 transition 组件一样;

总结

1、<transtion-group> 组件是多个子组件动画过渡,它渲染的是真实的元素;
2、当插入或者删除元素时会触发相应元素自身的过渡动画;
3、<transtion-group> 还实现了 move 的过渡效果;

下一篇:Vue2.x 源码 - event

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值