7

易懂好上手的transition-group函数式复用组件

 3 years ago
source link: https://segmentfault.com/a/1190000038856343
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

易懂好上手的transition-group函数式复用组件

Vue.js中内置了强大好用的过渡和动画辅助系统,让我们在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。Vue提供的 <transition><transition-group>的封装组件,可以包裹在我们需要添加进入/离开过渡的元素和组件的外面。可是,这两个组件只是提供了一个可以配置效果的外包装骨架,如果我们想把某些常用的过渡动画效果重复使用在我们的组件上的时候,就只能每次都重新同样来一次次配置在transition和transition-group上了吗?

不,就算我们想这样做,Vue也不希望我们这样做,在官方文档里的“过渡&动画>进入/离开&列表过渡>可复用的过渡”这个章节里提到:

要创建一个可复用过渡组件,你需要做的就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了。

函数式组件更适合完成这个任务。

ok,这就是我们今天的主题,如何制作一个可复用的 <transition-group>函数式组件。

嗯?这官网上不是有么?有是有,但是那是直接用函数做的组件,纯代码式创建组件对大伙儿看着其实不怎么友好,能有SFC(单文件组件)看着直观么?所以今天做的就是用SFC做的可复用的 <transition-group>函数式组件~~
注:本文用到的是Vue 2.x的版本,后期小伙伴可以自行迁移到3.x上~~

函数式组件

为什么写成函数式组件

先来做一下必要的说明,为什么说函数式组件做<transition-group>会更好呢?首先从业务上来说,我们的过渡组件并需要不关心自己底下子元素,也不需要给子元素传递什么属性,它就是一个一心一意给子元素提供过渡的组件:有子元素出现,触发过渡钩子,呈现进场的过渡效果;有子元素变化,提供旧位置移动到新位置的过渡动画效果;有子元素消失,呈现退场效果。所以它可以不需要任何状态,如果一个组件不需要状态的话,我们就可以考虑将其写成无状态的函数式组件,而在Vue 2.x中对函数式组件有一定性能优化,初始化速度比有状态组件快得多。这就是我们为什么把复用的<transition-group>写成函数式组件的主要原因。不过下面你就会发现,写成函数式组件的另外一个原因。

函数式组件怎么写

在官方文档上,其实已经写得很清楚了,纯代码的就不说了,这个不是我们今天要关注的,就先略过;单文件组件上写函数式组件只要简单地在template标签上写上functional,就成了:

<template functional>
</template>

非常简单!

此外,所有传进来的props都直接可以在模板上进行访问,不需要在script标签上的对象上进行声明;而且如果你没有任何需要自行定义或者额外配置的东西的话,连script标签也可以不用写!就像这样:

<template functional>
    <h2>{{props.title}}</h2>
</template>

上面就是一个最简单的函数组件,简直太好写了有没有!

当然,当你需要对props里面的属性进行计算的时候,还是可以写写script标签的,比如你需要对文字做一些转换:

<template functional>
    <h2>{{$options.coverTitle(props.title)}}</h2>
</template>
<script>
export default {
    coverTitle(str) {
    return str.toLowerCase();
  }
}
</script>

这时候,你可以使用$options来访问到你定义在script标签中的对象。

又或者你想绑定一些简单事件:

<template functional>
    <h2 @click="$option.clickTitle">{{props.title}}</h2>
</template>
<script>
export default {
    clickTitle(title) {
    alert('这是标题');
  }
}
</script>

都是可以的。

不过要注意的是,在script对象里面的代码是没有this的,也不能访问到文档中提到context里面的字段。

所以这也是为啥我们用函数式组件的原因,因为太简单了!

开始干活!

首先先确认需求,我们要做一个含有多种过渡效果的包含<transition-group>的组件,然后可以根据外部的传进来的参数来定制我们所需要的效果。

就这么简单!

先来把<transition-group>写进我们的函数式组件里!

<template functional>
    <transition-group
          :name="props.effectName"            
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>

外部传进去的effectName属性就直接绑定到<transition-group>就成!完事!

啊等等!不是这样的,你听我解释!这只是个示例!别关掉浏览器页面!

我们当然想要复用的过渡效果当然不是这么简单的啦,所以我们要做一点复杂的东西,才能有复用的价值,所以我们要做这个效果:
录制_2021_01_06_16_52_51_226.gif
这个依次进入的效果是不是看着很复杂!但是使用了<transition-group>就可以非常简单的完成!我们接着写!

官网中也有个例子类似这个效果的,思路就是使用了<transition-group>的JavaScript钩子,再结合data attribute,得到节点的index,然后进行相对应延迟渲染。动画的写法使用了Velocity.js。我们也可以来试试。

上面说过,可以在script标签中定义一些方法,然后可以在template上使用$options来访问得到,所以我们可以这样写:

<template functional>
    <transition-group
          v-on:before-enter="$options.beforeEnter"
          v-on:enter="$options.enter"
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
export default {
    beforeEnter(el) {
    // ...
  },
  enter(el, done) {
    // ...
  },
}
</script>

我们就在这写进入效果,其他效果同理的。然后就可以在这两函数里写效果了,先写好beforeEnter:

<template functional>
    <transition-group
          v-on:before-enter="$options.beforeEnter"
          v-on:enter="$options.enter"
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
export default {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(-50%)';
      el.style.transition = 'all 1s';
  },
  enter(el, done) {
    // ...
  },
}
</script>

beforeEnter设置好元素进入前的样式状态,然后要记得把transition属性设置上,这样才有过渡效果。

接下来写enter效果,我们希望进入之后是这样的状态:

<template functional>
    <transition-group
          v-on:before-enter="$options.beforeEnter"
          v-on:enter="$options.enter"
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
export default {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(-50%)';
      el.style.transition = 'all 1s';
  },
  enter(el, done) {
       dom.style.opacity = 1;
       dom.style.transform = 'translateX(0%)';
       dom.addEventListener('transitionend', done);
  },
}
</script>

很好,很简单!但是,这样写的话是没有效果的。

不知道出于什么原因(对,文档也没写,我也没想清楚),在只用JavaScript钩子的时候done回调函数是一定要有的,而且还不能同步调用,同步调用会失去效果,一定要异步进行,所以我们加上一点点的延时去执行这块的代码:

<template functional>
    <transition-group
          v-on:before-enter="$options.beforeEnter"
          v-on:enter="$options.enter"
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
export default {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(-50%)';
      el.style.transition = 'all 1s';
  },
  enter(el, done) {
       setTimeout(() => {
            el.style.opacity = 1;
            el.style.transform = 'translateX(0%)';
            el.addEventListener('transitionend', done);
      }, 100);
  },
}
</script>

这样过渡效果就生效了~~

但是这还不是我们想要的效果,这样写是同时进入的效果,不是我们想要的依次进入的效果,所以我们问题来了:官方文档中的效果是在用v-for渲染的同时给dom元素的data属性赋上子元素在集合中的index,那我们完全不知道我们transition-group将要包含多少个子元素,又应该如何知道他们的在集合中的顺序呢?

把他们当做vue的组件看的话,我们可能不知道,但是如果我们把他们当做dom节点来看的话呢?

只要知道子节点的前面有多少个兄弟节点,不就知道自己排第几了吗?

所以一个很简单的辅助函数就写出来了:

function getDomNodeIndex(el) {
  let node = el;
  let index = 0;
  while (node.previousSibling) { // 不断去访问前一个兄弟节点
    index += 1;
    node = node.previousSibling;
  }
  return index;
}

于是我们的组件就可以这样写:

<template functional>
    <transition-group
          v-on:before-enter="$options.beforeEnter"
          v-on:enter="$options.enter"
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
function getDomNodeIndex(el) {
  let node = el;
  let index = 0;
  while (node.previousSibling) { // 不断去访问前一个兄弟节点
    index += 1;
    node = node.previousSibling;
  }
  return index;
}
export default {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(-50%)';
      el.style.transition = 'all 1s';
  },
  enter(el, done) {
        const delay = getDomNodeIndex(el) * 200;
       setTimeout(() => {
            el.style.opacity = 1;
            el.style.transform = 'translateX(0%)';
            el.addEventListener('transitionend', done);
      }, delay);
  },
}
</script>

这样就完成了我们的一个效果!

进一步提高复用性

好了,我们的第一个效果就写好了,但是呢,我们不能满足于此,既然可以从左边滑进来,那是不是也可以从右边、从顶部、从底部滑进来?那既然可以依次滑进来,那是不是也可以一起从左边、从右边、从顶部、从底部滑进来?那既然可以滑进来了是不是也可以用其他效果写上?

行行行,慢慢来,接下来传授你进一步提高这个组件复用性的技巧。

上面都说了我们是函数式组件,所以根据外部传入的属性来形成不同的过渡效果就是我们接下来要攻克的难题。

先来提一下函数式组件的一个坑,就是template上无法动态绑定$options上的方法,这也许是Vue提高函数式组件渲染效率所做的一个措施,但是对于我们来说是有点不便的,具体来说是这样的:

<template functional>
    <transition-group
          v-on:before-enter="$options[props.animateName].beforeEnter"
          v-on:enter="$options[props.animateName].enter"           
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>

这里尝试使用通过props传进来的animateName属性来动态绑定进入的JavaScript钩子,但是没有生效。这样,我们就要给绕一下了。

怎么绕过这个限制呢?众所周知,Vue的模板中v-bind和v-on指令里可以写一些简单的js代码来实现简单的功能,所以我们这里可以这样:

<template functional>
    <transition-group
          v-on:before-enter="(el) => $options[props.animateType].beforeEnter(el)"
          v-on:enter="(el, done) => $options[props.animateType].enter(el, done)"          
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>

这样我们就可以巧妙绕过了无法动态绑定方法的限制了~~现在我们就可以在组件里定义多几个过渡效果函数了:

<template functional>
    <transition-group
         v-on:before-enter="(el) => $options[props.animateType].beforeEnter(el)"
          v-on:enter="(el, done) => $options[props.animateType].enter(el, done)" 
          tag="div"
          class="transition-group-container"
    >
      <slot></slot>
    </transition-group>
</template>
<script>
 function getDomNodeIndex(el) {
  let node = el;
  let index = 0;
  while (node.previousSibling) { // 不断去访问前一个兄弟节点
    index += 1;
    node = node.previousSibling;
  }
  return index;
}
export default {
  slideLeftInOrder: {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(-50%)';
      el.style.transition = 'all 1s';
      },
      enter(el, done) {
       const delay = getDomNodeIndex(el) * 200;
       setTimeout(() => {
            el.style.opacity = 1;
            el.style.transform = 'translateX(0%)';
            el.addEventListener('transitionend', done);
      }, delay);
      },
  },
  slideRightInOrder: {
    beforeEnter(el) {
      el.style.opacity = 0;
      el.style.transform = 'translateX(50%)';
      el.style.transition = 'all 1s';
      },
      enter(el, done) {
       const delay = getDomNodeIndex(el) * 200;
       setTimeout(() => {
            el.style.opacity = 1;
            el.style.transform = 'translateX(0%)';
            el.addEventListener('transitionend', done);
      }, delay);
      },
  },
  // ...更多的过渡效果
}
</script>

好啦,一个好用的transition-group函数式复用组件这样就写好,上面就是所有的源码,非常的好用简单!

参考:Vue.js
本文原创,未经授权不可转载


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK