戴兜

戴兜的小屋

Coding the world.
github
bilibili
twitter

!important導致TransitionGroup失效

image

大家如果曾經接觸過 Vue, 那麼大抵會對其自帶的組件 TransitionGroup 有所了解。這篇文章便記錄了 TransitionGroup 中「移動動畫」的一些使用細節。

或許你對 TransitionGroup 的「移動動畫」還不太了解,那麼我在這裡淺淺地介紹一下。正常使用時,你需要為 .[name]-move 類提供一個過渡樣式,例如 transition: all 0.5s ease;,這樣,當 TransitionGroup 內的元素位置變更後, Vue 會嘗試讓變動了位置的元素從老位置平滑過渡到新位置。當然,Vue 也支持新增元素和刪除元素的過渡效果,只需要為 [name]-enter-from[name]-leave-to 類名提供樣式,這不是本文的重點,故不再詳細介紹。

曾經的我,也像大部分人那樣按部就班把樣式寫完,沒出過問題。

直到群裡有人告訴我:「試試給元素增加一個常駐的、帶有 !importanttransition 樣式,會讓過渡失效」

image

我當場愣住😧,這在當時的我看來是一件很難理解的事情:本身過渡時 Vue 就會通過 [name]-move 為元素加上 transition 屬性,為什麼提前給元素加上一個優先級最高的 transition 屬性,過渡反而沒法生效了呢?

從源碼入手#

我們可以在 TransitionGroup.ts 閱讀與 TransitionGroup 相關的代碼內容

初始化階段#

不難發現,Vue 在渲染函數內,將子元素數組 children 賦值給 setup 函數作用域下的變量 prevChildren (L113)。同時,通過一個 for 循環遍歷 prevChildren (L135),將每個子元素的位置信息儲存於 positionMap 中 (舊位置)。

該階段中與我們討論內容相關的,便是這兩處內容。prevChildren 的賦值,使得 Vue 能夠在之後的 updated 生命週期中,得以取得子元素引用,方便進行相關操作。而 positionMap,讓 Vue 有能力在之後的操作中,得到元素的原始位置。

此處的 positionMap 是一個 WeakMap,Vue 使用元素對象作為 key 值,能夠保證在元素被銷毀後,positionMap 中對應元素的位置信息被適時自動回收。

Updated 生命週期#

當 TransitionGroup 內的子元素發生變動後,會調用 onUpdated 註冊的回調函數,同時也是在這裡,Vue 完成了過渡所需要的大部分操作。

首先,Vue 通過一個 forEach,再次遍歷獲取了各個元素的位置信息,儲存到 newPositionMap 中 (L72)。此時,一個元素的新舊位置分別儲存在 newPositionMappositionMap 中,我們需要做的,就是讓元素從舊位置平滑過渡到新位置。

接下來就是關鍵所在:既然是 updated 生命週期,此時元素們應該已經在新位置呆著了,又談何過渡?所以,我們要做的,並不是單純讓元素從舊位置過渡到新位置。而是將已經位於新位置的元素,重新放回舊位置,再讓其平滑返回到新位置,完成整個過渡過程。那么 Vue 是怎麼完成的呢?

L73的代碼是這樣的:

const movedChildren = prevChildren.filter(applyTranslation) 

Vue 使用 applyTranslation 方法過濾出了需要移動的子元素數組 movedChildren。通過觀察函數和變量的命名,我們幾乎可以肯定地說,在過濾的同時,Vue 還對元素進行了一些操作,而實際上也確實如此。

function applyTranslation(c: VNode): VNode | undefined {
  const oldPos = positionMap.get(c)!
  const newPos = newPositionMap.get(c)!
  const dx = oldPos.left - newPos.left
  const dy = oldPos.top - newPos.top
  if (dx || dy) {
    const s = (c.el as HTMLElement).style
    s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
    s.transitionDuration = '0s'
    return c
  }
}

對於需要移動的元素,Vue 計算了新舊位置差值,使用 css 屬性 Transform,將元素放回舊位置。特別關注一下 s.transitionDuration = '0s',~ 之後要考~。

但是實際上,此時的元素仍然沒有回到舊位置。瀏覽器會將樣式變動加入渲染隊列中,而不是立刻渲染。這裡涉及瀏覽器重排 (reflow) 的相關知識,可以搜索相關文章來進行閱讀。

為了保證元素立刻被放置到舊位置,在 L73 得到 movedPosition 後,Vue 執行了 forceReflow 方法 (L76),強制觸發重排。而 forceReflow 方法內容也很簡單 (Transition.ts#L461)

function forceReflow() {
  return document.body.offsetHeight
}

這裡也是個前端小知識🤯,讀取文檔的 offsetHeightoffsetWidth,也能觸發文檔重排。可以這麼理解:渲染隊列中存在改動而不進行重排直接獲取文檔寬度或高度,會導致拿到的元素寬高是過時的,所以瀏覽器在讀取前對文檔進行了重排。

之後的工作就很簡單了,只需要給元素加上 [name]-move 類名 (L81),然後去除之前添加的 transitionDurationTransform 屬性,元素自然就能平滑返回到新位置啦~監聽 transitionend 事件 (L83-L93),做一些收尾工作(去除過渡相關類名等)

讀到這裡,我們已經能夠解決文章開頭的那個問題了。實現過渡效果,需要確保元素正位於舊位置。在 Vue 中,為了確保文檔重排後元素通過 Transform 放到了舊位置,Vue 將元素的過渡時間設置為 0s 並進行了一次強制重排。但是人為添加的高優先級 transition 屬性導致重排時元素沒法第一時間回到舊位置,也就沒有過渡效果了。

我也寫了一個小 demo,簡化了 TransitionGroup 中的無關代碼,感興趣可以看看 https://codepen.io/DaiDR/pen/VwdMRxa

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。