關(guān)于Vue3過渡動(dòng)畫的踩坑記錄
背景
在我的 《Vue 3 開發(fā)企業(yè)級(jí)音樂 App》課程問答區(qū),有個(gè)同學(xué)提了個(gè)問題,在歌手列表到歌手詳情頁面到轉(zhuǎn)場(chǎng)動(dòng)畫中,只有進(jìn)入動(dòng)畫,卻沒有離場(chǎng)動(dòng)畫:

該學(xué)生確實(shí)在這個(gè)問題上研究了有一段時(shí)間,而且從他的描述,我一時(shí)半會(huì)兒也想不出哪有問題,于是讓他把代碼傳到 GitHub 上,畢竟直接從代碼層面定位問題是最靠譜的。
問題定位
一般遇到此類問題的時(shí)候,我的第一反應(yīng)是他用的 Vue 3 版本可能有問題,畢竟 Vue 3 還在不斷迭代過程,某個(gè)版本有一些小 bug 是很正常的,于是我把他的項(xiàng)目的 Vue 3 版本升級(jí)到了最新的 3.2.26。
但運(yùn)行后發(fā)現(xiàn),該問題仍然存在。我感到有些困惑,于是跑了一下自己課程項(xiàng)目源碼,并沒有復(fù)現(xiàn)該問題,然后我又把自己課程項(xiàng)目的 Vue 3 版本也升級(jí)到最新,仍然沒有復(fù)現(xiàn)該問題。
通過上述分析,我基本排除了 Vue 3 版本的問題。本質(zhì)上說,從歌手頁面切換到歌手詳情頁無非就是打開歌手詳情頁這個(gè)二級(jí)路由頁面,而從歌手詳情頁退回到歌手頁面無非就是移除歌手詳情頁這個(gè)二級(jí)路由頁面。于是我開始對(duì)比兩邊項(xiàng)目的歌手頁面以及詳情頁的源碼:
<!-- singer.vue -->
<template>
<div class="singer" v-loading="!singers.length">
<index-list
:data="singers"
@select="selectSinger"
></index-list>
<!-- 用router-view去承載二級(jí)路由 -->
<!-- <router-view :singer="selectedSinger"></router-view>-->
<!-- vue3需要在router-view中使用transition, appear進(jìn)入時(shí)候也會(huì)有動(dòng)畫 -->
<router-view v-slot="{ Component }">
<!-- singer-detail返回動(dòng)畫無效 研究 -->
<transition appear name="slide">
<!-- component動(dòng)態(tài)組件Component就是作用域插槽中的一個(gè)屬性,這個(gè)是由router-view這個(gè)組提供的
Component就是你的路由表中的路由組件
exclude="singer-detail"排除不緩存數(shù)據(jù)的組件否則會(huì)緩存數(shù)據(jù)導(dǎo)致每次數(shù)據(jù)都不重新請(qǐng)求
-->
<component :is="Component"
:singer="selectedSinger"
></component>
</transition>
</router-view>
</div>
</template>
<!-- singer-detail.vue -->
<template>
<!-- 因?yàn)橥ㄟ^二級(jí)路由實(shí)現(xiàn),所以放在views下 -->
<section class="singer-detail">
<music-list
:songs="songs"
:title="title"
:pic="pic"
:loading="loading"
></music-list>
</section>
</template>
上邊是學(xué)生的代碼,接下來貼一下我項(xiàng)目的源碼:
<!-- 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>
經(jīng)過對(duì)比,我感覺兩邊的源碼差別并不大,除了該學(xué)生會(huì)用注釋做一些學(xué)習(xí)筆記。一時(shí)間難以找出問題,于是我祭出了殺手锏——調(diào)試源碼。因?yàn)楫吘箤?duì)于 Vue 3 過渡動(dòng)畫的實(shí)現(xiàn)原理,我還是如數(shù)家珍的。
如果執(zhí)行了退出過渡動(dòng)畫,則一定會(huì)執(zhí)行 transition 組件包裹的子節(jié)點(diǎn)解析出的 leave 鉤子函數(shù)。
于是我在 leave 鉤子函數(shù)內(nèi)部加了個(gè) debugger 斷點(diǎn):
// @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 */);
}
// ...
}
接著運(yùn)行項(xiàng)目,當(dāng)我從歌手詳情頁回退到歌手頁面的時(shí)候,發(fā)現(xiàn)并沒有進(jìn)入 debugger 斷點(diǎn),也就意味著 leave 鉤子函數(shù)壓根沒有執(zhí)行。
再往前追溯,對(duì)于即將卸載的節(jié)點(diǎn),執(zhí)行其 leave 鉤子函數(shù)的時(shí)機(jī)是在執(zhí)行 remove 函數(shù)時(shí),于是我在 remove 函數(shù)內(nèi)部打上斷點(diǎn):
// @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();
}
};
接著再次運(yùn)行項(xiàng)目,當(dāng)我從歌手詳情頁回退到歌手頁面的時(shí)候,雖然進(jìn)入了斷點(diǎn),但也發(fā)現(xiàn)了一些代碼的邏輯問題:從 vnode 解析到了對(duì)應(yīng)的 transition 對(duì)象,由于其對(duì)應(yīng)的 type 是 Fragment,執(zhí)行進(jìn)入了下面這段邏輯:
if (type === Fragment) {
removeFragment(el, anchor);
return;
}
直接返回并沒有執(zhí)行后續(xù) transition 對(duì)象的 leave 鉤子函數(shù)。我繼續(xù)查看 vnode 的值,發(fā)現(xiàn)它有兩個(gè)子節(jié)點(diǎn),一個(gè)注釋節(jié)點(diǎn)和一個(gè) section 節(jié)點(diǎn)。我恍然大悟,原來是學(xué)生寫的注釋導(dǎo)致的問題:
<!-- singer-detail.vue -->
<template>
<!-- 因?yàn)橥ㄟ^二級(jí)路由實(shí)現(xiàn),所以放在views下 -->
<section class="singer-detail">
<music-list
:songs="songs"
:title="title"
:pic="pic"
:loading="loading"
></music-list>
</section>
</template>
在 Vue 的模版解析中,遇到 HTML 注釋,也會(huì)把它解析成一個(gè)注釋節(jié)點(diǎn),可以借助 Vue 3 的模版導(dǎo)出工具看一下它編譯后的結(jié)果:
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(" 因?yàn)橥ㄟ^二級(jí)路由實(shí)現(xiàn),所以放在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 支持了模版可以有不止一個(gè)的根節(jié)點(diǎn),上述模版的根就會(huì)被解析成一個(gè) Fragment 節(jié)點(diǎn),這就導(dǎo)致了該組件在移除的時(shí)候并不會(huì)執(zhí)行對(duì)應(yīng)的過渡動(dòng)畫。
進(jìn)一步分析
那么為啥 Fragment 節(jié)點(diǎn)就不需要過渡動(dòng)畫呢?我找到了代碼對(duì)應(yīng)的提交注釋:
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 節(jié)點(diǎn)不可以有 transition 過渡。但這里還有一個(gè)問題,為什么這么寫不會(huì)影響進(jìn)入過渡動(dòng)畫呢?
因?yàn)樵谶\(yùn)行時(shí)執(zhí)行組件 render 函數(shù)渲染組件的子樹 subTree 的時(shí)候,renderComponentRoot 函數(shù)內(nèi)部做了一些特殊處理:
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
}
在通過執(zhí)行組件實(shí)例的 render 方法拿到渲染的子樹后,在開發(fā)環(huán)境下通過 getChildRoot 函數(shù)對(duì)注釋節(jié)點(diǎn)做了一層過濾,得到結(jié)果 root,并且給它的根節(jié)點(diǎn)繼承了其 parent vnode 的 transition 對(duì)象。但是注意到,整個(gè) renderComponentRoot 返回的還是 result 對(duì)象。
對(duì)于我們的示例 SingerDetil 歌手詳情組件,它的子樹 vnode 是一個(gè) Fragment,但是在執(zhí)行 renderComponentRoot 的時(shí)候,由于第一個(gè)節(jié)點(diǎn)是注釋節(jié)點(diǎn),則被過濾,只有后面的實(shí)體節(jié)點(diǎn) singer-detail 對(duì)應(yīng)的 vnode 才有 transition 屬性,因此它有進(jìn)入過渡動(dòng)畫。
但是在組件移除的時(shí)候,由于組件的子樹 vnode 是一個(gè) Fragment,因此不會(huì)有離開過渡動(dòng)畫。
總結(jié)
找到了 bug 的原因后,修復(fù)就很簡(jiǎn)單了,直接把注釋節(jié)點(diǎn)刪除即可,當(dāng)然生產(chǎn)環(huán)境不會(huì)有該問題,因?yàn)樵谀J(rèn)情況下,生產(chǎn)環(huán)境會(huì)刪除注釋節(jié)點(diǎn)。
從這個(gè)案例來看,寫注釋雖然是個(gè)好習(xí)慣,但是一不小心可能會(huì)踩了 Vue 3 的坑。
學(xué)會(huì)源碼調(diào)試還是很重要的,如果不了解源碼,遇到此類 bug 就會(huì)一臉懵逼,非常被動(dòng),因?yàn)槲臋n不會(huì)告訴你原因。因此我還是鼓勵(lì)大家多學(xué)習(xí)源碼,通過調(diào)試源碼,你才能最接近事實(shí)的真相。?
到此這篇關(guān)于關(guān)于Vue3過渡動(dòng)畫的踩坑記錄的文章就介紹到這了,更多相關(guān)Vue3過渡動(dòng)畫踩坑內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中pinia數(shù)據(jù)一直重復(fù)獲取之前的值的解決方法
這篇文章主要介紹了vue中pinia數(shù)據(jù)一直重復(fù)獲取之前的值的解決方法,如果想讓pinia數(shù)據(jù)不會(huì)重復(fù)獲取之前的值需要手動(dòng)強(qiáng)制觸發(fā) Pinia store 的狀態(tài)更新,文中有詳細(xì)的解決方法,需要的朋友可以參考下2024-04-04
自定義input組件如何實(shí)現(xiàn)拖拽文件上傳
這篇文章主要介紹了自定義input組件如何實(shí)現(xiàn)拖拽文件上傳問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
Vue模擬響應(yīng)式原理底層代碼實(shí)現(xiàn)的示例
最近去面試的人都會(huì)有這個(gè)體會(huì),去年面試官只問我怎么用vue,今年開始問我vue響應(yīng)式原理,本文就詳細(xì)的介紹一下2021-08-08
使用vue-cli3 創(chuàng)建vue項(xiàng)目并配置VS Code 自動(dòng)代碼格式化 vue語法高亮問題
這篇文章主要介紹了使用vue-cli3 創(chuàng)建vue項(xiàng)目,并配置VS Code 自動(dòng)代碼格式化 vue語法高亮問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-05-05
Vue實(shí)現(xiàn)仿iPhone懸浮球的示例代碼
這篇文章主要介紹了Vue實(shí)現(xiàn)仿iPhone懸浮球的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
vue3子組件上綁定(v-model="xx")父組件傳過來的值后報(bào)錯(cuò)解決
這篇文章主要給大家介紹了關(guān)于vue3子組件上綁定(v-model="xx")父組件傳過來的值后報(bào)錯(cuò)解決辦法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用vue3具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-07-07
Vue項(xiàng)目中使用jquery的簡(jiǎn)單方法
這篇文章主要給大家介紹了關(guān)于Vue項(xiàng)目中使用jquery的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Vue具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05

