关于Vue3过渡动画的踩坑记录
作者:黄轶黄老师 发布时间:2024-06-07 16:03:35
背景
在我的 《Vue 3 开发企业级音乐 App》课程问答区,有个同学提了个问题,在歌手列表到歌手详情页面到转场动画中,只有进入动画,却没有离场动画:
该学生确实在这个问题上研究了有一段时间,而且从他的描述,我一时半会儿也想不出哪有问题,于是让他把代码传到 GitHub 上,毕竟直接从代码层面定位问题是最靠谱的。
问题定位
一般遇到此类问题的时候,我的第一反应是他用的 Vue 3 版本可能有问题,毕竟 Vue 3 还在不断迭代过程,某个版本有一些小 bug 是很正常的,于是我把他的项目的 Vue 3 版本升级到了最新的 3.2.26。
但运行后发现,该问题仍然存在。我感到有些困惑,于是跑了一下自己课程项目源码,并没有复现该问题,然后我又把自己课程项目的 Vue 3 版本也升级到最新,仍然没有复现该问题。
通过上述分析,我基本排除了 Vue 3 版本的问题。本质上说,从歌手页面切换到歌手详情页无非就是打开歌手详情页这个二级路由页面,而从歌手详情页退回到歌手页面无非就是移除歌手详情页这个二级路由页面。于是我开始对比两边项目的歌手页面以及详情页的源码:
<!-- singer.vue -->
<template>
<div class="singer" v-loading="!singers.length">
<index-list
:data="singers"
@select="selectSinger"
></index-list>
<!-- 用router-view去承载二级路由 -->
<!-- <router-view :singer="selectedSinger"></router-view>-->
<!-- vue3需要在router-view中使用transition, appear进入时候也会有动画 -->
<router-view v-slot="{ Component }">
<!-- singer-detail返回动画无效 研究 -->
<transition appear name="slide">
<!-- component动态组件Component就是作用域插槽中的一个属性,这个是由router-view这个组提供的
Component就是你的路由表中的路由组件
exclude="singer-detail"排除不缓存数据的组件否则会缓存数据导致每次数据都不重新请求
-->
<component :is="Component"
:singer="selectedSinger"
></component>
</transition>
</router-view>
</div>
</template>
<!-- singer-detail.vue -->
<template>
<!-- 因为通过二级路由实现,所以放在views下 -->
<section class="singer-detail">
<music-list
:songs="songs"
:title="title"
:pic="pic"
:loading="loading"
></music-list>
</section>
</template>
上边是学生的代码,接下来贴一下我项目的源码:
<!-- singer.vue -->
<template>
<div class="singer" v-loading="!singers.length">
<index-list
:data="singers"
@select="selectSinger"
></index-list>
<router-view v-slot="{ Component }">
<transition appear name="slide">
<component :is="Component" :data="selectedSinger"/>
</transition>
</router-view>
</div>
</template>
<!-- singer-detail.vue -->
<template>
<div class="singer-detail">
<music-list
:songs="songs"
:title="title"
:pic="pic"
:loading="loading"
></music-list>
</div>
</template>
经过对比,我感觉两边的源码差别并不大,除了该学生会用注释做一些学习笔记。一时间难以找出问题,于是我祭出了杀手锏——调试源码。因为毕竟对于 Vue 3 过渡动画的实现原理,我还是如数家珍的。
如果执行了退出过渡动画,则一定会执行 transition 组件包裹的子节点解析出的 leave 钩子函数。
于是我在 leave 钩子函数内部加了个 debugger 断点:
// @vue/runtime-core/dist/runtime.core-bundler.esm.js
leave(el, remove) {
debugger
const key = String(vnode.key);
if (el._enterCb) {
el._enterCb(true /* cancelled */);
}
// ...
}
接着运行项目,当我从歌手详情页回退到歌手页面的时候,发现并没有进入 debugger 断点,也就意味着 leave 钩子函数压根没有执行。
再往前追溯,对于即将卸载的节点,执行其 leave 钩子函数的时机是在执行 remove 函数时,于是我在 remove 函数内部打上断点:
// @vue/runtime-core/dist/runtime.core-bundler.esm.js
const remove = vnode => {
debugger
const { type, el, anchor, transition } = vnode;
if (type === Fragment) {
removeFragment(el, anchor);
return;
}
if (type === Static) {
removeStaticNode(vnode);
return;
}
const performRemove = () => {
hostRemove(el);
if (transition && !transition.persisted && transition.afterLeave) {
transition.afterLeave();
}
};
if (vnode.shapeFlag & 1 /* ELEMENT */ &&
transition &&
!transition.persisted) {
const { leave, delayLeave } = transition;
const performLeave = () => leave(el, performRemove);
if (delayLeave) {
delayLeave(vnode.el, performRemove, performLeave);
}
else {
performLeave();
}
}
else {
performRemove();
}
};
接着再次运行项目,当我从歌手详情页回退到歌手页面的时候,虽然进入了断点,但也发现了一些代码的逻辑问题:从 vnode 解析到了对应的 transition 对象,由于其对应的 type 是 Fragment,执行进入了下面这段逻辑:
if (type === Fragment) {
removeFragment(el, anchor);
return;
}
直接返回并没有执行后续 transition 对象的 leave 钩子函数。我继续查看 vnode 的值,发现它有两个子节点,一个注释节点和一个 section 节点。我恍然大悟,原来是学生写的注释导致的问题:
<!-- singer-detail.vue -->
<template>
<!-- 因为通过二级路由实现,所以放在views下 -->
<section class="singer-detail">
<music-list
:songs="songs"
:title="title"
:pic="pic"
:loading="loading"
></music-list>
</section>
</template>
在 Vue 的模版解析中,遇到 HTML 注释,也会把它解析成一个注释节点,可以借助 Vue 3 的模版导出工具看一下它编译后的结果:
import { createCommentVNode as _createCommentVNode, resolveComponent as _resolveComponent, createVNode as _createVNode, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { class: "singer-detail" }
function render(_ctx, _cache) {
const _component_music_list = _resolveComponent("music-list")
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createCommentVNode(" 因为通过二级路由实现,所以放在views下 "),
_createElementVNode("section", _hoisted_1, [
_createVNode(_component_music_list, {
songs: _ctx.songs,
title: _ctx.title,
pic: _ctx.pic,
loading: _ctx.loading
}, null, 8 /* PROPS */, ["songs", "title", "pic", "loading"])
])
], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */))
}
由于 Vue 3 支持了模版可以有不止一个的根节点,上述模版的根就会被解析成一个 Fragment 节点,这就导致了该组件在移除的时候并不会执行对应的过渡动画。
进一步分析
那么为啥 Fragment 节点就不需要过渡动画呢?我找到了代码对应的提交注释:
fix(fragment): perform direct remove when removing fragments This avoids trying to grab .el from hoisted child nodes (which can be created by another instance), and also skips transition check since fragment children cannot have transitions.
注释给的解释就是 Fragment 节点不可以有 transition 过渡。但这里还有一个问题,为什么这么写不会影响进入过渡动画呢?
因为在运行时执行组件 render 函数渲染组件的子树 subTree 的时候,renderComponentRoot 函数内部做了一些特殊处理:
function renderComponentRoot(instance) {
let result
// ...
// call render funtion to get the result
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result;
let setRoot = undefined;
if ((process.env.NODE_ENV !== 'production') &&
result.patchFlag > 0 &&
result.patchFlag & 2048 /* DEV_ROOT_FRAGMENT */) {
[root, setRoot] = getChildRoot(result);
}
// inherit transition data
if (vnode.transition) {
// ...
root.transition = vnode.transition;
}
return result
}
在通过执行组件实例的 render 方法拿到渲染的子树后,在开发环境下通过 getChildRoot 函数对注释节点做了一层过滤,得到结果 root,并且给它的根节点继承了其 parent vnode 的 transition 对象。但是注意到,整个 renderComponentRoot 返回的还是 result 对象。
对于我们的示例 SingerDetil 歌手详情组件,它的子树 vnode 是一个 Fragment,但是在执行 renderComponentRoot 的时候,由于第一个节点是注释节点,则被过滤,只有后面的实体节点 singer-detail 对应的 vnode 才有 transition 属性,因此它有进入过渡动画。
但是在组件移除的时候,由于组件的子树 vnode 是一个 Fragment,因此不会有离开过渡动画。
来源:https://mp.weixin.qq.com/s/as9kv-PibvPsvTIv7IMHZw


猜你喜欢
- 一、相关代码数据库配置类 MongoDBConn.py#encoding=utf-8'''Mongo Conn连接类
- 本月的每月挑战会主题是NLP,我们会在本文帮你开启一种可能:使用pandas和python的自然语言工具包分析你Gmail邮箱中的内容。NL
- 需求是这样的,我从本科到现在硬盘里存了好多照片,本来是按类别分的,有一天,我突然想,要是能按照时间来分类可能会更好。可以右键查看照片的属性,
- 一、动机(Motivate)在我们的现实生活中有很多例子可以拿来说明这个模式,我们还拿吃饺子这个事情来说。我的奶奶说了,今天想吃饺子,发出了
- pandas中遍历dataframe的每一个元素假如有一个需求场景需要遍历一个csv或excel中的每一个元素,判断这个元素是否含有某个关键
- 高性能异步爬虫目的:在爬虫中使用异步实现高性能的数据爬取操作异步爬虫的方式:- 多线程、多进程(不建议):好处:可以为相关阻塞的操作单独开启
- 我们可以通过打开流并写入和读取 HTTP 请求和响应来使用 asyncio 查询网站的 HTTP 状态。然后我们可以使用 asyncio 并
- 显示图像是 Opencv最基本的操作之一, imshow()函数可以实现该操作。如果使用过其他GUI框架背景,就会很自然地调用 imshow
- 所有的计算机程序都可以大致分为两类:脚本型(单次运行)和连续运行型(直到用户主动退出)。脚本型:脚本型的程序包括最早的批处理文件以及使用Py
- Turtle库是Python语言中一个很流行的绘制图像的函数库,想象一个小乌龟,在一个横轴为x、纵轴为y的坐标系原点,(0,0)
- 前言一周没有更新博客了,这一周的时间内加强了对机器学习和图像处理的学习。学的有点混乱,有必要记录一下。深度学习可以解决很多问题,但有时候深度
- 在工作中,有时会遇见需要将数字转换为条码的问题,每次都需要打开条码转换的网站,一次次的转换后截图,一两个还行,但是当需要转换的数量较多时,就
- 1. 概述动态规划算法应用非常之广泛。对于算法学习者而言,不跨过动态规划这道门,不算真正了解算法。初接触动态规划者,理解其思想精髓会存在一定
- 简介使用百度深度学习框架paddlepaddle对人像图片进行自动化抠图安装根据PaddlePaddle官网命令安装如pip install
- 格式化是通过格式操作使任意类型的数据转换成一个字符串。例如下面这样<script>console.log(chopper.for
- 使用Python进行项目开发时,由于不同的项目需要,可能会配置多个开发环境,不同开发环境之间的项目依赖包如果混合在一起,可能会引起意想不到的
- 前面我们讲了 TCP 编程,我们知道 TCP 可以建立可靠连接,并且通信双方都可以以流的形式发送数据。本文我们再来介绍另一个常用的协议–UD
- Python中的闭包前几天又有人留言,关于其中一个闭包和re.sub的使用不太清楚。我在脚本之家搜索了下,发现没有写过闭包相关的东西,所以决
- Linux终端里面可谓是奇妙无限,很多优秀的软件都诞生在终端里面。相较之下,Windows本身的理念和Linux就不一致,所以,你懂得。 下
- 安装cesium在已有项目中执行,npm i cesium修改配置build/webpack.base.conf.js1、定义 Cesium