14.3.1 原生 DOM 的过渡
过渡动效的本质:元素在两种状态之间切换,浏览器根据「CSS 过渡效果」自动完成中间过程;做框架级过渡,就是把这条时间线拆成几个阶段并挂上回调。
一、过渡的三要素
| 要素 | 说明 |
|---|---|
| 初始状态 | 过渡开始前元素的样子(一个 CSS 类) |
| 结束状态 | 过渡结束后元素的样子(另一个 CSS 类) |
| 过渡效果 | 持续时长、运动曲线、要过渡的属性 —— 写在 transition 里 |
.box { width: 100px; height: 100px; background-color: skyblue; }
.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 el = document.createElement('div')
el.classList.add('box')
// 1. beforeEnter:元素被添加到页面之前,先定义初始状态和运动过程
el.classList.add('enter-from')
el.classList.add('enter-active')
document.body.appendChild(el) // 添加到页面
// 2. enter:在下一帧切换状态
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.remove('enter-from')
el.classList.add('enter-to')
// 3. 过渡结束:收尾
el.addEventListener('transitionend', () => {
el.classList.remove('enter-to')
el.classList.remove('enter-active')
})
})
})
| 阶段 | 动作 |
|---|---|
beforeEnter(插入页面前) | 添加 enter-from + enter-active |
enter(下一帧) | 移除 enter-from,添加 enter-to |
过渡结束(transitionend) | 移除 enter-to + enter-active |
为什么必须「先加初始状态,下一帧再切状态」:如果在同一帧内既加初始类又加结束类,浏览器不会认为发生了状态变化,过渡不会生效。
为什么用双层
requestAnimationFrame:Chrome / Safari 存在浏览器差异(Issue 675795),requestAnimationFrame注册的回调可能就在当前帧执行(除非此前已调用过一次 rAF);再嵌套一层即可保证回调落在下一帧。
三、离场过渡:等过渡结束再卸载
离场与进场同理,需要定义离场初始状态、结束状态与过渡过程;区别在于元素被卸载时不要立即卸载,而是在过渡结束后再卸载:
el.addEventListener('click', () => {
// 把卸载动作封装起来,延迟到过渡结束后执行
const performRemove = () => el.parentNode.removeChild(el)
// 1. 初始状态
el.classList.add('leave-from')
el.classList.add('leave-active')
// 2. 强制 reflow,让上面添加的初始状态真正生效
document.body.offsetHeight
// 3. 下一帧切换状态
requestAnimationFrame(() => {
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()
})
})
})
易错点:离场时必须强制 reflow(如读取
document.body.offsetHeight)。因为元素已经在页面上,若不在同一帧内让初始类生效就立刻切换状态,浏览器无法感知状态改变,过渡同样不生效。
transitionend只会在过渡真正发生后触发;若过渡时间为 0 或属性未变化,事件不会触发,收尾逻辑(以及performRemove)就不会执行。
四、阶段与回调的对应关系
这套「初始状态 → 下一帧切状态 → 过渡结束后收尾」的流程,就是后面 Transition 组件要抽象出来的钩子:
| 原生 DOM 步骤 | Transition 组件中的钩子 |
|---|---|
插入前加 xxx-from + xxx-active | beforeEnter(el) |
下一帧切到 xxx-to、监听 transitionend | enter(el) |
| 卸载前加离场类、过渡结束后再移除元素 | leave(el, performRemove) |
相关
- 14.3.2 实现 Transition 组件:把上面的类名与时机搬进渲染器,用 vnode 上的
transition钩子驱动过渡