1. 程式人生 > 程式設計 >關於Vue3過渡動畫的踩坑記錄

關於Vue3過渡動畫的踩坑記錄

目錄
  • 背景
  • 問題定位
  • 進一步分析
  • 總結

背景

在我的 《 3 開發企業級音樂 App》課程問答區,有個同學提了個問題,在歌手列表到歌手詳情頁面到轉場動畫中,只有進入動畫,卻沒有離場動畫:

關於Vue3過渡動畫的踩坑記錄

該學生確實在這個問題上研究了有一段時間,而且從他的描述,我一時半會兒也想不出哪有問題,於是讓他把程式碼傳到 GitHhttp://www.cppcns.comub 上,畢竟直接從程式碼層面定位問題是最靠譜的。

問題定位

一般遇到此類問題的時候,我的第一反應是他用的 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"排除不快取資料的元件否則會快取資料導致每次資料都不重新請求

   www.cppcns.com
--> <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.

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

      },8 /* PROPS */,["songs","title","pic","loading"])

    ])

  ],2112 /* STABLE_FRAGMENT,DEV_ROOT_FRAGMENT */))

}

由於 Vue 3 支援了模版可以有不止一個的根節點,上述模版的根就會被解析成一個 Fragment 節點,這就導致了該元件在移除的時候並不會執行對應的過渡動畫。

進一步分析

那麼為啥 Fragment 節點就不需要過渡動畫呢?我找到了程式碼對應的提交註釋:

fix(fragment): perform dir客棧ect 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 &&www.cppcns.comamp;

    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,因此不會有離開過渡動畫。

總結

找到了 bug 的原因後,修復就很簡單了,直接把註釋節點刪除即可,當然生產環境不會有該問題,因為在預設情況下,生產環境會刪除註釋節點。

從這個案例來看,寫註釋雖然是個好習慣,但是一不小心可能會踩了 Vue 3 的坑。

學會原始碼除錯還是很重要的,如果不瞭解原始碼,遇到此類 bug 就會一臉懵逼,非常被動,因為文件不會告訴你原因。因此我還是鼓勵大家多學習原始碼,通過除錯原始碼,你才能最接近事實的真相。

到此這篇關於關於Vue3過渡動畫的踩坑記錄的文章就介紹到這了,更多相關Vue3過渡動畫踩坑內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!