戴兜的小屋
戴兜的小屋

!important导致TransitionGroup失效

!important导致TransitionGroup失效

大家如果曾经接触过 Vue, 那么大抵会对其自带的组件 TransitionGroup 有所了解。这篇文章便记录了 TransitionGroup 中「移动动画」的一些使用细节。

或许你对 TransitionGroup 的「移动动画」还不太了解,那么我在这里浅浅地介绍一下。正常使用时,你需要为 .[name]-move 类提供一个过渡样式,例如 transition: all 0.5s ease;,这样,当 TransitionGroup 内的元素位置变更后, Vue 会尝试让变动了位置的元素从老位置平滑过渡到新位置。当然,Vue 也支持新增元素和删除元素的过渡效果,只需要为 [name]-enter-from[name]-leave-to 类名提供样式,这不是本文的重点,故不再详细介绍。

曾经的我,也像大部分人那样按部就班把样式写完,没出过问题。

直到群里有人告诉我:「试试给元素增加一个常驻的、带有 !importanttransition 样式,会让过渡失效」

https://cdn.daidr.me/wp-content/uploads/2022/11/Snipaste_2022-11-23_12-41-43.png

我当场愣住 :grin: ,这在当时的我看来是一件很难理解的事情:本身过渡时 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
}

这里也是个前端小知识 :razz:读取文档的 offsetHeightoffsetWidth,也能触发文档重排。可以这么理解:渲染队列中存在改动而不进行重排直接获取文档宽度或高度,会导致拿到的元素宽高是过时的,所以浏览器在读取前对文档进行了重排。

之后的工作就很简单了,只需要给元素加上 [name]-move 类名(L81),然后去除之前添加的 transitionDurationTransform 属性,元素自然就能平滑返回到新位置啦~监听 transitionend 事件(L83-L93),做一些收尾工作(去除过渡相关类名等)

读到这里,我们已经能够解决文章开头的那个问题了。实现过渡效果,需要确保元素正位于旧位置。在 Vue 中,为了确保文档重排后元素通过 Transform 放到了旧位置,Vue 将元素的过渡时间设置为 0s 并进行了一次强制重排。但是人为添加的高优先级 transition 属性导致重排时元素没法第一时间回到旧位置,也就没有过渡效果了。

我也写了一个小 demo,简化了 TransitionGroup 中的无关代码,感兴趣可以看看 https://codepen.io/DaiDR/pen/VwdMRxa

赞赏

戴兜

文章作者

发表回复

textsms
account_circle
email

                   验证码加载中...

  • 师门

    没接触到,但是被纯熟的代码写作所吸引

    3天前 回复

戴兜的小屋

!important导致TransitionGroup失效
大家如果曾经接触过 Vue, 那么大抵会对其自带的组件 TransitionGroup 有所了解。这篇文章便记录了 TransitionGroup 中「移动动画」的一些使用细节。 或许你对 TransitionGroup 的「移…
扫描二维码继续阅读
2022-11-23