为了账号安全,请及时绑定邮箱和手机立即绑定

用 transition-group 做轮播气泡的踩坑实践

标签:
Vue.js

轮播组件,不管是轮播 banner 图、轮播中奖名单还是轮播气泡,都可以抽象为一个数组进栈出栈的过程:

const array = [0, 1, 2, 3, 4]
const out = array.shift()
array.push(out)

轮播气泡尤为典型,一端的气泡 fadeout,同时另一端的气泡 fadeIn,其它气泡同步顺移,这个过程以固定时间间隔无限循环。

那么如何将上述数据结构映射为视图呢?我想到了 vue 内置的过渡动画组件:transition-group官方文档 语焉不详的描述虽然初看不太明白,不过照猫画虎,还是可以很自然的想到下列实现方案:

<template>
  <div id="app">
    <transition-group name="bubble" tag="ul">
        <li v-for="(val, i) in bubbles" :key="i">{{ val.text }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data () {
    return {
      bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }]
    }
  },
  mounted () {
    setInterval(() => {
      const out = this.bubbles.shift()
      this.bubbles.push(out)
    }, 2000)
  }
}
</script>

<style lang="scss">
  li {
    transition: all 1s;
    display: block;
  }
  .bubble-enter {
    opacity: 0;
    transform: translateY(30px);
  }
  .bubble-leave-to {
    opacity: 0;
    transform: translateY(-30px);
  }
  /* 这个看似无用,但必须加上 */
  .bubble-leave-active {
    position: absolute;
  }
</style>

由此我们踩到了第一个坑:确实轮播滚动起来了,但是没有渐变过渡的动画效果,单独使用 shift 或 push 都有,但是一起用就没了。中间的试探过程暂且不表,直接说结论:循环项的 key 必须和数组内容一样,也即 v-for="val in bubbles" :key="val"

stackoverflow 上也有人提到了这一点:

试探过程中我还发现了一个有意思的现象:当气泡发生位移时,在 devtools 中抓不到任何样式属性的变化,多出的一个 style 属性里什么都没有,新增的 class 名 bubble-move 并没有挂载任何样式。那么气泡到底是怎么动起来的呢?

文档说了,transition-group 是基于一个叫 flip 的动画队列,其实只要耐心阅读一小段 flip 的示例代码,就能解释上述现象,同时也容易理解官方文档里那个炫酷的棋盘 shuffle 动画的实现原理。

// Get the first position.
var first = el.getBoundingClientRect();

// Now set the element to the last position.
el.classList.add('totes-at-the-end');

// Read again. This forces a sync
// layout, so be careful.
var last = el.getBoundingClientRect();

// You can do this for other computed styles as well, if needed.
// Just be sure to stick to compositor-only
// props like transform and opacity.
var invert = first.top - last.top;

// Invert.
el.style.transform = `translateY(${invert}px)`;

// Wait for the next frame so we
// know all the style changes have taken hold.
requestAnimationFrame(function() {

  // Switch on animations.
  el.classList.add('animate-on-transforms');

  // GO GO GOOOOOO!
  el.style.transform = '';
});

// Capture the end with transitionend
el.addEventListener('transitionend',
    tidyUpAnimations);

回到气泡组件上,现在的问题是数组内都是对象,对象不能作为 key,于是考虑新增一个专门用于动画的数组,key 和数组的内容一致:

<template>
  <div id="app">
    <transition-group name="bubble" tag="ul">
        <li v-for="index in list" :key="index">{{ bubbles[index].text }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data () {
    return {
      bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }],
      list: [0, 1, 2, 3]
    }
  },
  mounted () {
    setInterval(() => {
      const out = this.list.shift()
      this.list.push(out)
    }, 2000)
  }
}
</script>

不过效果仍然不理想:

可以看到 bubble-enterbubble-leave-to 并没有添加到 class 中。

为了解决问题,中间走了一个大弯路。首先我用 setTimeout(() => { this.list.push(out) }, 0),得到了近乎最终的效果:

稍加了一些处理,终于实现了理想的效果,但是结果并不牢靠:iOS 上一切安好,在安卓上(app webview),一开始动画都是好的,但只要页面滑动两下,动画效果就混乱了,而且是必现的。手机浏览器里就没事,pc 上也没事。

滑动两下动画就乱了?偏偏是安卓 webview?跟定时器在移动端滚动过程中会中止有关?多次试验无法确证是否与此有关,换用 requestAnimationFrame 也不能解决问题。那这是安卓本身的 bug 吗?还是 transition-group 的 bug?不过就算是它们的 bug,问题也必须解决不是。我甚至一度想推翻重来,但拥挤的排期意味着我只能周末加班了,不甘呐。

码感敏锐的人可能发现了,我之前的用法看上去总有点不靠谱。文档既没说什么能做,也没说什么不能做,全靠自己试,于是生造了一些奇淫巧技骚操作。以前也遇到过两三个现象十分诡异的问题,但最终发现都是隐匿在某处的小细节造成的。我相信计算机不会错,错的一定是人。

于是我又去看文档,官方示例看上去是可以满足轮播场景的,同时执行 remove 和 add,数字移出和进入的动画可以同时进行,那么我的用法跟官方的区别到底在哪里呢

其实就差在这里:

官方示例每次添加的都是新元素 nextNum++,我每次加的都是老元素 list.shift(),这可能会影响到 flip 动画中 first 和 last 状态的计算。因此按照示例的做法一改就好了:

<template>
  <div id="app">
    <transition-group name="bubble" tag="ul">
        <li v-for="(index, i) in list" :key="index">{{ bubbles[i].text }}</li>
    </transition-group>
  </div>
</template>

<script>
export default {
  data () {
    return {
      bubbles: [{ text: 'a' }, { text: 'b' }, { text: 'c' }, { text: 'd' }],
      list: [0, 1, 2, 3],
      next: 10
    }
  },
  mounted () {
    setInterval(() => {
      this.list.shift()
      this.list.push(this.next++)
    }, 2000)
  }
}
</script>

不过问题还是很明显:内容并没有跟着气泡循环,之前我们用 bubbles[index] 中的数据,后来我们改用 next++ 表示新增数据,因此不能再用 bubbles[index]。解决方法也很容易想到:bubbles 跟随 list 同步循环不就好了吗?

this.list.shift()
this.list.push(this.next++)
const out = this.bubbles.shift()
this.bubbles.push(out)

嗯,看上去是期待中的样子。

如果只希望展示 3 个气泡,只需要把 list 改为 [0, 1, 2] 就好了。

最后再提示一点:气泡作为容器,最好不要直接在容器元素上设置样式,比如气泡的样式是运营配置的,那么最好将 style 设置在容器的子元素上,避免干扰容器样式的计算(参见 flip 动画一节)。

<li style={/* 样式配置 */}></li> // not good

<li>
    <div style={/* 样式配置 */}></div> // better
</li>

小结

1、最好完全按照官方示例使用 transition-group;

2、容器与内容解耦。容器只负责定位布局,内容只负责样式。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
7
获赞与收藏
16

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消