Vue.js 源码分析:transition-group 列表过渡实现
Vue.js 源码分析:transition-group 列表过渡实现
【免费下载链接】vue-analysis :thumbsup: Vue.js 源码分析 项目地址: https://gitcode.com/gh_mirrors/vu/vue-analysis
在前端开发中,我们经常需要为列表元素的添加、删除和排序提供平滑的过渡动画。Vue.js 提供的 <transition-group> 组件正是解决这一需求的强大工具。与只能处理单个元素过渡的 <transition> 组件不同,<transition-group> 专为列表过渡设计,能够同时处理多个元素的进入、离开和移动动画。本文将深入分析 <transition-group> 组件的实现原理,帮助你理解其内部工作机制。
基本用法与示例
让我们先通过一个简单示例了解 <transition-group> 的基本用法:
let vm = new Vue({
el: '#app',
template: '<div id="list-complete-demo" class="demo">' +
'<button v-on:click="add">Add</button>' +
'<button v-on:click="remove">Remove</button>' +
'<transition-group name="list-complete" tag="p">' +
'<span v-for="item in items" v-bind:key="item" class="list-complete-item">' +
'{{ item }}' +
'</span>' +
'</transition-group>' +
'</div>',
data: {
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)
}
}
})
配合以下 CSS 样式:
.list-complete-item {
display: inline-block;
margin-right: 10px;
}
.list-complete-move {
transition: all 1s;
}
.list-complete-enter, .list-complete-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-complete-enter-active {
transition: all 1s;
}
.list-complete-leave-active {
transition: all 1s;
position: absolute;
}
在这个示例中,点击 "Add" 按钮会随机插入一个新数字,点击 "Remove" 按钮会随机删除一个数字。通过 <transition-group> 组件和相应的 CSS 样式,实现了元素的进入、离开和移动过渡效果。
组件定义与基本结构
<transition-group> 组件的定义位于 src/platforms/web/runtime/components/transitions.js 文件中。它是一个非抽象组件,会渲染为一个真实的 DOM 元素,默认标签为 span,但可以通过 tag 属性自定义。
组件的基本结构如下:
export default {
props: extend({
tag: String,
moveClass: String
}, transitionProps),
beforeMount () {
// 重写 _update 方法
},
render (h: Function) {
// 渲染逻辑
},
updated () {
// 处理 move 过渡
},
methods: {
hasMove (el: any, moveClass: string): boolean {
// 判断是否有 move 过渡
}
}
}
与 <transition> 组件不同,<transition-group> 删除了 mode 属性,因为列表过渡不需要控制进入/离开的顺序。同时增加了 tag 和 moveClass 两个新属性,tag 用于指定渲染的元素标签,moveClass 用于自定义移动过渡的类名。
render 函数实现
<transition-group> 的 render 函数是其核心实现之一,负责处理子节点并准备过渡所需的数据。
变量初始化
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)
这里初始化了一些关键变量:
tag: 组件渲染的标签名map: 用于存储当前子节点的 key 映射prevChildren: 上一次渲染的子节点rawChildren: 当前插槽中的原始子节点children: 经过过滤后的有效子节点transitionData: 从组件属性中提取的过渡相关数据
处理子节点
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}>`)
}
}
}
这段代码遍历原始子节点,过滤出有效的子节点并存储到 children 数组中。特别需要注意的是,<transition-group> 要求每个子节点必须有唯一的 key,否则会在开发环境下发出警告。同时,将过渡数据 transitionData 添加到每个子节点的 data.transition 属性中,以便子节点能够触发过渡效果。
处理上一次的子节点
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)
这段代码处理上一次渲染的子节点 prevChildren,将其分为两类:
kept: 仍然存在于当前子节点中的节点removed: 已从当前子节点中移除的节点
对于每个上一次的子节点,记录其位置信息(通过 getBoundingClientRect()),并将过渡数据添加到节点中。最后,通过 h(tag, null, children) 渲染当前的子节点。
move 过渡实现
<transition-group> 最强大的功能是能够实现列表元素的移动过渡效果。这一功能主要在 updated 钩子函数中实现。
判断是否有 move 过渡
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
}
首先获取 moveClass,如果未自定义,则默认为 name + '-move' 或 v-move。然后通过 hasMove 方法判断是否定义了 move 过渡的样式。
hasMove 方法的实现:
hasMove (el: any, moveClass: string): boolean {
if (!hasTransition) {
return false
}
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)
}
hasMove 方法通过创建元素克隆,添加 moveClass 并检查是否有 transform 过渡,来判断是否需要执行 move 过渡动画。
处理子节点位置变化
// 三轮循环避免 DOM 读写混合,防止布局抖动
children.forEach(callPendingCbs)
children.forEach(recordPosition)
children.forEach(applyTranslation)
// 强制重排,使所有元素就位
this._reflow = document.body.offsetHeight
children.forEach((c: VNode) => {
if (c.data.moved) {
var el: any = c.elm
var s: any = el.style
addTransitionClass(el, moveClass)
s.transform = s.WebkitTransform = s.transitionDuration = ''
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
if (!e || /transform$/.test(e.propertyName)) {
el.removeEventListener(transitionEndEvent, cb)
el._moveCb = null
removeTransitionClass(el, moveClass)
}
})
}
})
这段代码是实现 move 过渡的核心,分为四个步骤:
- 处理未完成的回调:
callPendingCbs确保之前的过渡动画回调已执行 - 记录新位置:
recordPosition记录每个节点更新后的位置 - 应用平移变换:
applyTranslation计算新旧位置差异并应用 transform - 触发过渡动画:清除 transform,使元素从旧位置过渡到新位置
其中,applyTranslation 函数的实现:
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'
}
}
该函数计算节点新旧位置的差异,如果有差异,则通过 transform 将节点暂时移回旧位置,为后续的过渡动画做准备。
beforeMount 钩子函数
为了确保移除元素的过渡效果正常工作,<transition-group> 在 beforeMount 钩子中重写了 _update 方法:
beforeMount () {
const update = this._update
this._update = (vnode, hydrating) => {
// 强制执行移除过渡
this.__patch__(
this._vnode,
this.kept,
false, // hydrating
true // removeOnly (!important, avoids unnecessary moves)
)
this._vnode = this.kept
update.call(this, vnode, hydrating)
}
}
通过将 __patch__ 方法的第四个参数 removeOnly 设置为 true,确保在更新子节点时不会移动节点,只会移除需要删除的节点,从而保证移除过渡的正常执行。
总结
<transition-group> 组件通过以下几个关键步骤实现了列表过渡效果:
- 渲染真实元素:作为非抽象组件,渲染为指定标签的真实 DOM 元素
- 管理子节点:在
render函数中处理子节点,记录新旧位置信息 - 处理进入/离开过渡:为添加和删除的元素应用过渡效果
- 实现移动过渡:在
updated钩子中通过 transform 实现元素的平滑移动
通过这些机制,<transition-group> 能够为列表提供流畅的添加、删除和排序过渡效果,极大地提升了用户体验。
官方文档中还有更多关于 <transition-group> 的使用细节和示例,可以参考 docs/v2/extend/tansition-group.md 深入学习。掌握 <transition-group> 的使用和实现原理,将帮助你构建更加生动和友好的用户界面。
【免费下载链接】vue-analysis :thumbsup: Vue.js 源码分析 项目地址: https://gitcode.com/gh_mirrors/vu/vue-analysis
更多推荐


所有评论(0)