Teleport 弹窗尺寸为什么会丢(2):Transition 怎样协调创建和移除
用 v-if 关闭弹窗时,通常希望它先播放离场动画,再从页面移除。如果 Vue 在 open=false 时立即删掉 DOM,浏览器就没有元素可以继续播放动画。Transition 为此把条件切换与真正的 DOM 移除协调起来。
它还包含一个容易忽略的角色:内部的 BaseTransition 是组件,会在自己的 render 中调用默认插槽。理解动画钩子如何交给 renderer,也需要区分插槽在哪里声明、又是在谁的渲染任务中执行。
示例与源码固定使用 Vue 3.5.44 的 VDOM runtime,源码链接锁定对应 tag。
简单功能:给条件切换加进入和离开过程
<Transition> 为一个元素或组件在进入、离开时安排钩子。使用 name="fade" 时,DOM 版 Transition 可以在元素上切换 fade-enter-*、fade-leave-* 等 CSS 类,让浏览器执行动画;也可以使用 JavaScript 钩子控制完成时机。它本身同样不会增加一层 DOM 包裹。
这里先观察最常见的 v-if 路径。v-show 保留元素、切换显示状态,也能配合 Transition,但它不需要在每次打开时重新创建这个元素。Transition 官方说明列出了这些触发条件和 CSS 类。
实际表现:open 已经为 false,旧元素仍在离场
这个例子把过渡放慢到 800ms。先打开弹窗,等检查区显示「进入完成」,再点「开始离场」。此时 open 已经变成 false,旧弹窗仍会暂时留在 #modals,直到离场结束。
检查区会记录 before-enter、after-enter、before-leave、after-leave,并读取目标容器的节点。离场时看见旧元素,不表示 v-if 没生效:Vue 已经处理了分支切换,只是把实际 DOM 移除交给了过渡完成回调。
进入开始时,元素带有 *-enter-from 和 *-enter-active;随后 from 换成 to,active 保留到过渡结束。离场也经历 from、active、to 的切换,结束后才真正移除元素。图中用的是正常完成的 v-if 过渡;快速反向切换、取消和不同 mode 还会进入额外分支。
源码实现:DOM 回调、BaseTransition 和 renderer 分工
VNode 是一次渲染结果的描述;renderer 把它挂载到 DOM,或对比新旧 VNode 更新节点。组件的渲染 effect 负责执行 render 与同步 patch,并收集执行期间读取的响应式依赖。
浏览器中的 Transition.ts 负责 CSS 类和浏览器事件,核心运行时的 BaseTransition.ts 负责子节点与进入、离开规则。DOM 版 Transition 的入口很短,下面保留实际函数:
export const Transition: FunctionalComponent<TransitionProps> =
/*@__PURE__*/ decorate((props, { slots }) =>
h(BaseTransition, resolveTransitionProps(props), slots),
)
resolveTransitionProps() 把 name、时长、用户回调等整理成 BaseTransition 能消费的 hooks;默认插槽继续交给 BaseTransition。BaseTransition 的 setup() 返回一个 render 函数,这个内部 render 才会调用 slots.default() 生成当前子节点。下面只摘出这个调用现场,省略后续的子节点选择、hooks 安装和 mode 分支:
// packages/runtime-core/src/components/BaseTransition.ts
setup(props: BaseTransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
const state = useTransitionState()
return () => {
const children =
slots.default && getTransitionRawChildren(slots.default(), true)
// 后续选择有效子节点、设置过渡 hooks,并返回本次渲染结果。
}
}
插槽是稍后调用的函数。父组件把它交出去时,函数体里的 open 不一定已经被读取;等 BaseTransition 的 render 调用它,才计算 v-if="open" 应该返回元素还是注释。插槽执行时正在运行的渲染 effect,会收集这次对 open 的读取。
| 函数 | 谁调用它 | 负责什么 |
|---|---|---|
resolveTransitionProps() |
DOM 版 Transition 的函数式入口 | 把 CSS 过渡配置整理成进入、离开回调 |
useTransitionState() |
BaseTransition 的 setup | 保存是否已挂载、是否正在离开、是否正在卸载等状态 |
getTransitionRawChildren() |
BaseTransition 的 render | 展开 Fragment 等节点,供后续选择实际参与过渡的子节点 |
resolveTransitionHooks() |
BaseTransition 的 render | 构建进入、离开、取消等 hooks,协调用户回调和完成回调 |
setTransitionHooks() |
BaseTransition 的 render | 把 hooks 接到子 VNode;如果子节点是组件,再沿组件子树处理 |
nextFrame()、whenTransitionEnds() |
DOM 进入、离开回调 | 用两次 requestAnimationFrame 安排切类,等待结束事件或超时后调用完成回调 |
这些 hooks 最后由 renderer.ts 执行。挂载元素时,renderer 在插入前调用 beforeEnter,插入后安排 enter;移除元素时,先把实际移除函数交给 leave,等待它完成再移除。这样 Transition 能控制动画时序,同时复用 renderer 原本的挂载、卸载逻辑。
如果只想观察组件与插槽更新,可以使用 <Transition :css="false">。它关闭的是 DOM 版 Transition 自动加 CSS 类和监听 CSS 动画结束的那一层,BaseTransition、默认插槽和内部组件的渲染过程仍然存在。因此,关闭 CSS 动画仍会保留内部组件的渲染和子节点 patch;它与直接移除 Transition 不等价。
总结
DOM 版 Transition 准备 CSS 类、结束事件与用户回调,BaseTransition 为当前子 VNode 安排过渡 hooks,renderer 在插入和移除节点时执行它们。离场开始后,状态已经关闭,真实节点仍可保留到完成回调。
默认插槽是在内部组件 render 时调用的。响应式依赖按实际读取时正在执行的 effect 收集,因此内部子树可以独立更新;关闭 CSS 动画并不会移除这条组件与插槽路径。
系列目录见Teleport 弹窗尺寸为什么会丢。上一篇:Teleport 弹窗尺寸为什么会丢(1):Teleport 怎样管理两个 DOM 位置。下一篇:Teleport 弹窗尺寸为什么会丢(3):CSS v-bind 怎样编译与更新变量。