关键在于利用 Vue 的 remove 钩子触发退出动画:在钩子中添加 CSS 类、监听 transitionend 后调用 rm(),配合唯一 key 和响应式数据更新确保正确 patch。

要实现带动画的列表删除效果,关键不是直接从 DOM 中移除元素,而是让 Vue 知道“这个节点即将消失”,从而触发过渡动画。渲染函数中没有 v-if 或 <transition-group></transition-group> 模板语法,必须手动控制 vnode 的生命周期,而 patch 钩子(更准确地说,是 insert、remove 和 update 钩子)正是介入这一过程的核心机制。
理解 remove 钩子:删除前的最后机会
remove 钩子在 vnode 即将被卸载时调用,此时真实 DOM 元素仍存在,但 Vue 已决定将其移除。这是启动退出动画的黄金时机——你可以在钩子中阻止立即删除,等待 CSS 动画或 JS 动画完成后再真正移除。
- 钩子接收两个参数:
vnode(当前 vnode)和rm(一个必须调用的回调函数,表示“现在可以安全删除了”) - 不能直接操作
vnode.el的样式而不调用rm,否则会内存泄漏或状态不一致 - 典型做法:给元素添加退出 class → 监听
transitionend→ 在事件回调里调用rm()
在 render 函数中为列表项注入 remove 钩子
每个待删除的列表项 vnode 都需配置 hook: { remove }。注意:该钩子只对有真实 DOM 元素的 vnode 生效(即组件根节点或普通元素 vnode)。
示例(简化版):
render(h) {
return h('ul', this.list.map((item, i) => {
return h('li', {
key: item.id,
// 关键:为每个 li 注入 remove 钩子
hook: {
remove: (vnode, rm) => {
const el = vnode.elm;
el.classList.add('fade-out');
el.addEventListener('transitionend', () => {
el.classList.remove('fade-out');
rm(); // 真正卸载
}, { once: true });
}
}
}, item.text);
}));
}
CSS 配合(确保有过渡属性):
.fade-out {
opacity: 0;
transform: translateX(20px);
transition: opacity 0.3s, transform 0.3s;
}
配合数据更新与 key 机制确保正确 patch
动画生效的前提是 Vue 能精准识别哪些节点要进入/离开。这依赖两点:
-
唯一且稳定 key:每个列表项必须用不会复用的 key(如
item.id),避免因 key 相同导致就地复用,跳过 remove 流程 -
响应式数据驱动更新:删除动作应通过修改响应式数组(如
this.list.splice(index, 1))触发 re-render,Vue 才会比对新旧 vnode 并调用对应钩子 - 不要手动调用
vm.$forceUpdate(),它绕过 diff,可能导致钩子不触发
进阶:支持批量删除与动画队列
一次删多个项时,多个 remove 钩子会并发触发。若希望动画错开或统一控制,可借助计数器或 Promise 队列:
- 用闭包或组件 data 记录当前正在动画的项数,全部完成后执行收尾逻辑
- 在 remove 钩子中返回 Promise,并在
update或父级 watch 中 await 所有动画结束(需配合自定义指令或组合式逻辑) - 更稳妥的方式:使用
requestAnimationFrame延迟首次样式变更,避免浏览器跳过过渡
不复杂但容易忽略。

















