14.3.2 实现 Transition 组件
Transition 组件本身不渲染任何额外内容:它只是把默认插槽里的过渡元素取出,在它的 vnode 上挂
transition钩子,由渲染器在元素挂载 / 卸载的不同阶段调用。
一、设计思路
- Transition 基于虚拟 DOM 实现:把整个过渡过程抽象成几个阶段(
beforeEnter、enter、leave等),渲染器需要在元素生命周期的特定阶段执行对应回调; - Transition 组件只做两件事:通过默认插槽读取过渡元素、在它的 vnode 上添加 transition 相关钩子;
- 真正在什么时候调用钩子,由渲染器决定。
二、渲染器侧改造
1. 挂载元素时
function mountElement(vnode, container, anchor) {
const el = (vnode.el = createElement(vnode.type))
// ...处理 children / props
// 判断 vnode 是否需要过渡
const needTransition = vnode.transition
if (needTransition) {
// 插入页面之前:调用 beforeEnter
vnode.transition.beforeEnter(el)
}
insert(el, container, anchor)
if (needTransition) {
// 插入页面之后:调用 enter
vnode.transition.enter(el)
}
}
2. 卸载元素时
function unmount(vnode) {
const needTransition = vnode.transition
// ...Fragment / 组件分支
const parent = vnode.el.parentNode
if (parent) {
// 把真正的移除动作封装起来,交给 leave 决定何时执行
const performRemove = () => parent.removeChild(vnode.el)
if (needTransition) {
// 需要过渡:交给 leave,过渡结束后由它调用 performRemove
vnode.transition.leave(vnode.el, performRemove)
} else {
// 不需要过渡:直接移除
performRemove()
}
}
}
顺序很关键:
beforeEnter在insert之前(先摆好初始状态,再上页面),enter在insert之后(元素已在页面上,下一帧切状态才会产生过渡)。
三、Transition 组件实现
const Transition = {
name: 'Transition',
setup(props, { slots }) {
return () => {
// 通过默认插槽获取需要过渡的元素
const innerVNode = slots.default()
// 在过渡元素的 vnode 上添加 transition 钩子
innerVNode.transition = {
beforeEnter(el) {
// 设置初始状态:添加 enter-from 和 enter-active
el.classList.add('enter-from')
el.classList.add('enter-active')
},
enter(el) {
// 在下一帧切换到结束状态
nextFrame(() => {
el.classList.remove('enter-from')
el.classList.add('enter-to')
// 监听 transitionend 做收尾
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
})
},
leave(el, performRemove) {
// 离场初始状态
el.classList.add('leave-from')
el.classList.add('leave-active')
// 强制 reflow,让初始状态生效
document.body.offsetHeight
// 在下一帧切换状态
nextFrame(() => {
el.classList.remove('leave-from')
el.classList.add('leave-to')
el.addEventListener('transitionend', () => {
el.classList.remove('leave-to')
el.classList.remove('leave-active')
// 过渡结束,调用第二个参数完成 DOM 卸载
performRemove()
})
})
}
}
// 渲染需要过渡的元素本身
return innerVNode
}
// 双层 rAF:保证回调在下一帧执行
function nextFrame(cb) {
requestAnimationFrame(() => {
requestAnimationFrame(cb)
})
}
}
}
四、钩子与 CSS 类名的对应关系
官方实现里类名会带上 name 前缀(默认 v-,如 <Transition name="fade"> 则变成 fade-enter-from);本例省略了前缀,直接用下面的类名:
| 阶段 / 钩子 | 添加的类 | 移除的类 | 时机 |
|---|---|---|---|
beforeEnter | enter-from、enter-active | — | 元素插入页面之前 |
enter | enter-to | enter-from | 插入后的下一帧 |
transitionend(进场) | — | enter-to、enter-active | 进场过渡结束 |
leave | leave-from、leave-active | — | 卸载时,先强制 reflow |
leave(下一帧) | leave-to | leave-from | reflow 之后的下一帧 |
transitionend(离场) | — | leave-to、leave-active | 然后调用 performRemove() 真正卸载 |
对应 CSS 仍然是 14.3.1 的那套:
.enter-from { transform: translateX(200px); }
.enter-to { transform: translateX(0); }
.enter-active { transition: transform 1s ease-in-out; }
.leave-from { transform: translateX(0); }
.leave-to { transform: translateX(200px); }
.leave-active { transition: transform 2s ease-out; }
五、测试用例
const trigger = ref(false)
const CompVNode = {
type: Transition,
children: {
default() {
return { type: 'p', children: '我是需要过渡的元素 testA...' }
}
}
}
setTimeout(() => {
trigger.value = !trigger.value
}, 2000)
effect(() => {
// 挂载时走 beforeEnter / enter,卸载时走 leave
renderer.render(trigger.value ? null : CompVNode, document.querySelector('#app'))
})
六、易错点
过渡只作用于单个根元素:Transition 组件通过默认插槽拿到一个 vnode 并在它身上挂
transition,插槽里放多个元素时无法处理。
离场的
leave里必须强制 reflow(读document.body.offsetHeight),否则初始类与新类在同一帧内一起生效,过渡不触发,performRemove也永远等不到transitionend。
库与 DOM 过渡的关系:CSS 过渡类名 / 钩子既可以直接手写,也可以交给第三方动画库(在钩子里调用库的 API),框架只负责在正确时机调用钩子。
相关
- 14.3.1 原生 DOM 的过渡:CSS 类名、
requestAnimationFrame与transitionend的原始写法 - 14.2.2 实现 Teleport 组件:同样把渲染逻辑从渲染器中分离出去的内建组件