上一章我们看来一下<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>
组件是⾮抽象组件,它会渲染成⼀个真实元素, 默认 tag
是 span
;
1、遍历
<transtition-group>
包裹的原始⼦节点,初始化 children,拿到每一个vnode
,判断vnode
是否设置了key
,没设置则会报错;设置了把vnode
添加到children
中,然后把提取的数据transitionData
添加的vnode.data.transition
中,这样 才能实现列表中单个元素的过渡动画;
2、当上一次子节点存在,遍历获取vnode
,然后把transitionData
赋值到vnode.data.transition
,这个是为了当它在enter
和leave
的钩⼦函数中有过渡动画;
3、接着⼜调⽤了原⽣ DOM 的getBoundingClientRect
⽅法获取到原⽣ DOM 的位置信息,记录到vnode.data.pos
中,然后判 断⼀下vnode.key
是否在map
中,如果在则放⼊kept
中,否则表⽰该节点已被删除,放⼊removed
中;
4、然后通过执⾏h(tag, null, kept)
渲染后放⼊this.kept
中,把removed
⽤this.removed
保存;
5、最后整个render
函数通过h(tag, null, children)
⽣成渲染vnode
;
update
在元素插入和删除的时候,会重新执行 render
函数来渲染新的节点,还要触发 update
钩子函数;
1、利用 hasMove
判断⼦元素是否定义 move 相关样式,不存在直接返回;
hasMove
首先克隆一个 DOM 节点,然后为了避免影响而移除它的过渡 class ,接着添加了moveClass
样式,设置display
为none
,添加到组件根节点上;接下 来通过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>
组件多了 tag
和 moveClass
属性;
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
的过渡效果;