记手撸一个轮播组件的踩坑之旅

说起轮播图,就会想起自己刚学js以及css动画的那段日子,也曾经用原生js什么的去实现过,也说得出不同实现方式的优劣。之后呢,做项目都是用框架,用过layui、jq-weui什么的,都是直接用他们封装好的轮播图组件。直到最近学了Vue,看着他2.0有动画过渡什么的,就想着自己尝试着封装一下,之前没有造轮子的经验,写的不好好请多指教。

先上一下效果图:

因为前前后后做了两种实现,第二种是在第一种的基础上优化的(其实第一种没做完),所以分开来讲。

版本一

版本一的思路是按照掘金上的教程来的 这里贴一下教程地址 作者:limingru 侵联立删

需求分析

  1. 在点击右侧箭头时,图片向左滑动到下一张;点击左侧箭头时,图片向右滑到下一张
  2. 过渡效果,图片滑动
  3. 小圆点指示当前图片(当前图片对应的小圆点有特殊样式)
  4. 无限滚动,即在滚动到最后一张时,再点击下一张时会继续向左滑动到第一张

原理讲解

图中红线区域即是我们的视口(其他图片会被隐藏)。 这个轮播只展示5张图片,但是在它的首尾各还有两张图片,在图1前面放置了图5,在图5后面放置了图1。 之所以这么做,是为了做无限滚动。 无限滚动的原理在于:当整个图向左侧滚动到右边的图5时,会继续向前走到图1,在完全显示出图1后,会切换到最左边的图1。 这样,即使再向左侧滑动看到的就是图2了。 如下图:在最后的图1完成过渡完全显示出来后,再将整个列表瞬间向右拉到左侧的图1。 另一张边界图图5的滚动也是,不过方向相反。

接下来,我们怎么实现移动呢? 其实很简单,我们只要改变我们的视口(如图红色框框)的样式, 比如说他的初始样式:

transform: translate3d(-888px, 0,0);
复制代码

我们改变为

transform: translate3d(0px, 0,0);
复制代码

视口就向左移动一张图片的宽度了(这里图片宽度为888px) 然后,我们只要给左右按钮一个点击事件,点击的时候传递参数(移动距离,移动方向) 就可以移动视口

代码架构

  1. html架构
<div class="slide">
    <!-- 视窗 -->
    <div class="window">
        <!-- 图片区域 -->
        <ul class="container"
        :style="containerStyle">
            <li><img :src="sliders[sliders.length-1].img" alt=""></li>
            <li v-for="(item, index) in sliders"><img :src="item.img" alt=""></li>
            <li><img :src="sliders[0].img" alt=""></li>
        </ul>

        <!-- 左右箭头 -->
        <div class="direction">
            <div @click="move(888,1)" class="left-btn icon-box">
                <i class="icon">&#xe617;</i>
            </div>
            <div @click="move(888,-1)" class="right-btn icon-box">
                <i class="icon">&#xe616;</i>
            </div>
        </div>

        <!-- 下面的小点 -->
        <div class="dots-box">
            <div v-for="(item,index) in sliders"
            class="dot" :class="{'dot-active': cur == index'}"></div>
        </div>
    </div>
</div>
<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
    let sliders = new Vue({
        el: '.slide',
        data: {
            sliders: [
            {
                img: 'imgs/0.jpg'
            },
            {
                img: 'imgs/1.jpg'
            },
            {
                img: 'imgs/2.jpg'
            },
            {
                img: 'imgs/3.jpg'
            },
            {
                img: 'imgs/4.jpg'
            }
            ],
            cur: 0,
            distance: -888,
        },
        computed: {
            containerStyle() {  //这里用了计算属性,用transform来移动整个图片列表
                console.log('------change containerStyle-------');
                  return {
                    transform:`translate3d(${this.distance}px, 0, 0)`
                  }
                }
        },
        methods: {
            move(offset, direction) {
                this.distance += offset * direction;
                if( direction == 1) {
                    this.cur--;
                } else {
                    this.cur++;
                }
                if(this.cur == 5 ) this.cur = 0;
                if(this.cur == -1) this.cur = 4;

                console.log('------set distance-------');
                if (this.distance < -4440) {
                    console.log('------set distance 4440to888-------');
                    this.distance = -888;
                }
                if (this.distance > -888) {
                    console.log('------set distance 888to4440-------');
                    this.distance = -4440;

                }

            }
        }
    })
</script>
复制代码

简单讲解下关键的布局要求:

  1. window会有一个overflow:hidden的样式,所以页面上其实是存在一排的图片,而我们只看得到视口位置的图片
  2. 上面原理图所标志的红色框就是container,为了可以在初始时显示图一而不是图五,我们得给他一个transform: translate3d(${-图片宽度}px, 0, 0)的样式
  3. 为了实现图片并排而不是挤下来,我们给container display:flex
  4. 为了实现图片移动的时候有动画过渡,我们再给container transition: all .5s;

有关Vue的代码解释

  1. v-for指令,遍历在js中注册的data中的sliders数组
<li v-for="(item, index) in sliders"><img :src="item.img" alt=""></li>
复制代码
  1. @click绑定事件,给元素绑定move()这个事件,事件在Vue实例中声明
<div class="direction">
    <div @click="move(888,1)" class="left-btn icon-box">
        <i class="icon">&#xe617;</i>
    </div>
    <div @click="move(888,-1)" class="right-btn icon-box">
        <i class="icon">&#xe616;</i>
    </div>
</div>
复制代码
  1. :class绑定类名 踩坑实例:
<!-- 下面的小点 -->
<div class="dots-box">
    <div v-for="(item,index) in sliders"
    class="dot" :class="{dot-active: cur == index'}"></div>
</div>
复制代码

我刚开始这样写,直接报错 原因很简单,绑定类名的时候,如果类名带有-等,则需要这样写

<div class="dot" :class="{'dot-active': cur == index'}"></div>
复制代码

在这里上个效果图:

很明显可以看出,在最后一张继续点击向右,会很明显的反方向拉到第一张,根据原作者的解决思路,是要在从第五张啦,然后接下来是再拉向右是右边的第一张,(所以在html那里会有 <li><img :src="sliders[0].img" alt=""></li>),然后等动画完成后,再瞬间切换到左边的第一张,我稍微想了想,决定放弃这种实现方式,改用我自己的思路。 所以做到这里,我决定重新构建

版本二

vue列表过渡

大家可以看到,这里的插入删除的时候,每个数字因为布局(某个元素插入或者删除导致位置变更)而产生的移动是可以做出动画效果的,这就给了我启发,我何不用这个特性来做图片轮播呢

踩坑实例

模仿着官网demo

<head>
    <meta charset="UTF-8" />
    <title>测试</title>
    <style>
        .list-item {
          display: inline-block;
          margin-right: 10px;
        }
        .list-enter-active, .list-leave-active {
          transition: all 1s;
        }
        .list-enter, .list-leave-to
        /* .list-leave-active for below version 2.1.8 */ {
          opacity: 0;
          transform: translateY(30px);
        }
        .list-move {
            transition: transform 1s;
        }



        .fade-enter-active, .fade-leave-active {
            transition: all 1s;
        }
        .fade-enter, .fade-leave-to {
            opacity: 0;
            transform: translateY(50px);
        }
        .fade-move {
            transition: all 1s;
        }
        .li-item {
            display: inline-block;
        }
        .li-img {
            width: 100px;
            margin-right: 10px;
        }
    </style>
</head>
<body>
<div id="list-demo" class="demo">
  <button v-on:click="add">Add</button>
  <button v-on:click="remove">Remove</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
  <transition-group name="fade" tag="ul">
    <li v-for="(item, index) in sliders"  class="li-item">
        <img class="li-img" :src="item.img" alt="">
    </li>
  </transition-group>
</div>

<script src="../vue.js"></script>
<script>
    new Vue({
      el: '#list-demo',
      data: {
        items: [1,2,3,4,5,6,7,8,9],
        nextNum: 10,
        sliders: [
            {
                img: 'imgs/0.jpg'
            },
            {
                img: 'imgs/1.jpg'
            },
            {
                img: 'imgs/2.jpg'
            },
            {
                img: 'imgs/3.jpg'
            },
            {
                img: 'imgs/4.jpg'
            }
            ],
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.sliders.length)
        },
        add: function () {
          this.items.splice(this.randomIndex(), 0, this.nextNum++);
          this.sliders.splice(this.randomIndex(), 0, {img: 'imgs/4.jpg'})
        },
        remove: function () {
          this.items.splice(this.randomIndex(), 1);
          this.sliders.splice(2,1);
        },
      }
    })
</script>
</body>
复制代码

但是发现结果并不是我想的那样

可以看到,图片并没有因为位置变化而平滑过度,而是瞬移的,简单说就是-move没有作用 这是怎么回事呢? 其实问题在于 v-for的复用 解决: 给li-img 设置独有的key值 不然vue只是在原来基础上更换src而已 并没有插入删除dom元素造成-move没有效果

官方:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。

其实从原理分析,为什么要复用 我是这么理解的:我们在一个数组[0,1,2,3]第二个元素的位置插入一个4,变成[0,4,1,2,3] 一般按照常理我们做法是,直接在第二个元素插入4,然后后面的向后移动一个位置 但是,学过数组插入的我们都知道,数组插入的耗费很大 所以会有另一个办法:把第二个元素的值改成4,之后的依次拿前面的值,再在最后添加原来最后的值(比如说这里的3) 以上都是本人一本正经瞎解释的,如果有人知道底层实现的话麻烦指导我一下。

知道了问题所在,不就是设置一个key吗,简单

<transition-group name="fade" tag="ul">
    <li v-for="(item, index) in sliders"  key="index" class="li-item">
        <img class="li-img" :src="item.img" alt="">
    </li>
  </transition-group>
复制代码

然和我就写出了这样的代码,可以运行一下发现,依然无效 这又是怎么回事? 稍一分析,不难发现,这里的key值我用了index,这就是问题所在 设想一下,我们在插入/删除的瞬间,这个index其实也是会跟着变化的,这就达不到key预期所要标志元素的效果 改进一下

//html
<transition-group name="fade" tag="ul">
    <li v-for="(item, index) in sliders" :key="item.tag" class="li-item">
        <img class="li-img" :src="item.img" alt="">
    </li>
</transition-group>

//js
new Vue({
      el: '#list-demo',
      data: {
        items: [1,2,3,4,5,6,7,8,9],
        nextNum: 10,
        sliders: [
            {
                tag: 0,
                img: 'imgs/0.jpg'
            },
            {
                tag: 1,
                img: 'imgs/1.jpg'
            },
            {
                tag: 2,
                img: 'imgs/2.jpg'
            },
            {
                tag: 3,
                img: 'imgs/3.jpg'
            },
            {
                tag: 4,
                img: 'imgs/4.jpg'
            }
            ],
            tag: 5
      },
      methods: {
        randomIndex: function () {
          return Math.floor(Math.random() * this.sliders.length)
        },
        add: function () {
          this.items.splice(this.randomIndex(), 0, this.nextNum++);
          this.sliders.splice(this.randomIndex(), 0, {img: 'imgs/4.jpg' ,tag: this.tag++})
        },
        remove: function () {
          this.items.splice(this.randomIndex(), 1);
          this.sliders.splice(2,1);
        },
      }
复制代码

可以发现我给slider每个元素一个独一无二的tag

可以发现,在插入图片的时候,可以符合我们的预期,但是,删除的时候
阿勒,你怎么瞬移呀?内心好崩溃
在网上查了一下解决方式后,发现需要给fade-leave-active这个类加一个绝对定位

//css
.fade-leave-active {
  position: absolute;
}

复制代码

个人理解:这个类的作用是元素在被删除,进入离开直至动画结束都保有的一个类 在原来没加的时候,动画完成之前,元素一直占据他的布局位置,直到结束后才瞬间失去他的位置,造成其他元素瞬移(这时其他元素没有-move变化延时) 绝对定位之后,刚进入动画的时候就失去位置,其他元素被出发-move的变化延时

完美实现

原理分析

我们顺着vue列表过渡这个思路来 再对图片数量进行优化,要实现轮播效果,其实我们页面只需要三个图片元素

页面常规状态

图片向右滑动

在pre的前面,插入pre-1对应的图片,以此让pre,cur,next布局位置向右移动一个图片位置,实现突破向右滑动,之后删除掉next就行 结束后,又变成常规状态,上一个状态的pre-1,pre,cur分别对应现在的pre,cur,next

图片向左滑动

删除pre,实现cur,next布局位置向左滑动一个图片位置,实现图片向左滑动,之后在next后面添加一个next+1就行 结束后,变成常规状态,上一个状态的cur,next,next+1变成现在的pre,cur,next

//没有overflow:hidden
<body>
<div class="slide">
    <!-- 视窗 -->
    <div class="window" id="sliders">
        <!-- 图片区域 -->
        <transition-group name="fade" class="container" tag="ul" >
            <li v-for="(item,index) in sliders" :key="item.tag">
                <img :src="imgs[item.imgIndex]" alt="">
            </li>
        </transition-group>


        <!-- 左右箭头 -->
        <div class="direction">
            <div @click="toRight()" class="left-btn icon-box">
                <i class="icon">&#xe617;</i>
            </div>
            <div @click="toLeft()" class="right-btn icon-box">
                <i class="icon">&#xe616;</i>
            </div>
        </div>

        <!-- 下面的小点 -->
        <div class="dots-box">
            <div v-for="(item,index) in imgs" :key="index" class="dot"
            :class="{'dot-active': index === cur}"></div>
        </div>
    </div>

</div>

<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
    let sliders = new Vue({
        el: '.slide',
        data: {
            imgs: ['imgs/0.jpg','imgs/1.jpg','imgs/2.jpg','imgs/3.jpg','imgs/4.jpg'],
            sliders: [
            {
                imgIndex: 0,
                tag: 0
            },
            {
                imgIndex: 1,
                tag: 1
            },
            {
                imgIndex: 2,
                tag: 2
            }
            ],
            tag: 3,
            cur: 1,
            lock: true
        },
        computed: {

        },
        methods: {
            toRight: function() {
                this.sliders.pop();

                let insert = this.sliders[0].imgIndex-1;
                if(insert == -1) insert = 4;
                this.sliders.unshift({ imgIndex: insert, tag: insert});


                if( --this.cur == -1) this.cur = 4;

            },
            toLeft: function() {

                let insert = this.sliders[this.sliders.length-1].imgIndex+1;
                if(insert == 5) insert = 0;
                this.sliders.push({ imgIndex:insert, tag: insert});

                this.sliders.shift();

                if( ++this.cur == 5) this.cur = 0;
            }
        }
    })
</script>
</body>
复制代码

可以看到,连续快速点击会造成节点过快删除而出现白色大块 这个也很好解决,稍微节流一下 这里值得注意的是:在使用setTimeout的时候this指针的指向问题 这里使用箭头函数解决

//加锁
//1s后开锁
if( !this.lock ) return;
this.lock = false;
setTimeout(()=>{
    this.lock = true;
},1000);
复制代码

至此,轮播图开发完毕

等等,怎么还没完

标题写的很清楚,这次是开发一个轮播图组件 说到组件,vue给我们实现的机制非常便利,我们在封装东西的时候也十分简单

//slide.js
var sliders_component = {
    template:
    `
        <div class="window" >
            <transition-group name="fade" class="container" tag="ul" >
                <li v-for="(item,index) in sliders" :key="item.tag">
                    <img :src="sonimgs[item.imgIndex]" alt="">
                </li>
            </transition-group>
            <div class="direction">
                <div @click="toRight()" class="left-btn icon-box">
                    <i class="icon">&#xe617;</i>
                </div>
                <div @click="toLeft()" class="right-btn icon-box">
                    <i class="icon">&#xe616;</i>
                </div>
            </div>
            <div class="dots-box">
                <div v-for="(item,index) in sonimgs" :key="index" class="dot"
                :class="{'dot-active': index === cur}"></div>
            </div>
        </div>
    `,
    props: [
        'sonimgs', 'length'
    ],
    data: function(){
        return {

            sliders: [
            {
                imgIndex: 0,
                tag: 0
            },
            {
                imgIndex: 1,
                tag: 1
            },
            {
                imgIndex: 2,
                tag: 2
            }
            ],
            tag: 3,
            cur: 1,
            lock: true
        };
    },
    methods: {
        toRight: function() {
            //加锁
            //1s后开锁
            if( !this.lock ) return;
            this.lock = false;
            setTimeout(()=>{
                this.lock = true;
            },1000);


            this.sliders.pop();

            let insert = this.sliders[0].imgIndex-1;
            if(insert == -1) insert = this.length-1;
            this.sliders.unshift({ imgIndex: insert, tag: insert});


            if( --this.cur == -1) this.cur = this.length-1;

        },
        toLeft: function() {
            //加锁
            //1s后开锁
            if( !this.lock ) return;
            this.lock = false;
            setTimeout(()=>{
                this.lock = true;
            },1000);

            let insert = this.sliders[this.sliders.length-1].imgIndex+1;
            if(insert == this.length) insert = 0;
            this.sliders.push({ imgIndex:insert, tag: insert});

            this.sliders.shift();

            if( ++this.cur == this.length) this.cur = 0;
        }
    }
}

复制代码

这个js文件中定义了一个sliders组件 我们在使用的时候,只需要引入对应的css,vue原件,还有这个slide.js文件

<div id="app">
    <sliders :sonimgs=ParentImgs :length=ParentImgs.length ></sliders>
</div>

<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
    new Vue({
        el: '#app',
        components: {
            sliders: sliders_component
        },
        data: {
            ParentImgs: ['imgs/0.jpg','imgs/1.jpg','imgs/2.jpg','imgs/3.jpg','imgs/4.jpg'],
        }
    })
</script>
复制代码

在使用的时候通过父子通信传递图片链接就行了。

end

转载于:https://juejin.im/post/5ab661c95188255580023280

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值