阅读 642

从0到1,带你脱离类库重新写一个高端的无缝自动轮播图!

前言

前几天看到某位大佬的一篇名为《你们都被VUE惯坏了》的文章,没怎么细读,大概意思就是拿轮播图举例,来告诫各位开发者脱离框架外还是要掌握JS的基础功能,要多了解各种库实现的原理,虽然那篇文章有说到轮播图的一些实现思想,但是要实现一个库,要注意的细节还是很有很多,所以决定写下这一篇文章,重新回味一遍JS版的轮播图

涉及到的知识点

我所写的轮播图90%都是由JS来实现,HTMLCSS只是用来构建组件容器和样式,其中会用到ES6的Proxy以及requestAnimationFrame,在这篇文章中也会随带着说一下这方面的知识点,但是不会细讲。

实现思路

无缝轮播图最灵魂的一个点应该就是无缝轮播了,所谓无缝轮播,就是你点击那一张,效果上都是从下一张or上一张平滑的划过去,在来一个合适的速度,给人的感觉就很舒畅。接下来一张图能完全表现出其思路。 就如图中的步骤:

  1. 点击右侧按钮,在当前轮播图父容器中将下一张图片加进去。
  2. 改变现在两张图的位置,也就是left,使第二张图的位置滑到第一张图,要有过渡效果。
  3. 销毁第一张图,使第二张图变成第一张图。

不管是左滑还是右滑,其思路就是在当前图的前边/后边在塞进去一张图,完全塞好以后,在进行移动。上图中我没有在底部加上当前图片的在第几个的小圆点,若是从第一张直接跳到第五张也是这个效果。

代码实现

基础的HTML以及CSS

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }
      .carousel {
        position: relative;
        margin: auto;
        overflow: hidden;
      }
      .imgCon {
        position: absolute;
        left: 0;
        right: 0;
      }
    </style>
  </head>
  <body>
    <div class="carousel">
      <!-- 轮播图的最外层的容器 -->
      <div class="imgCon"></div>
      <!-- 轮播图图片的容器-->
    </div>
    <script>
      /*
        JS代码在这里
      */
    </script>
  </body>
</html>
复制代码

carousel为轮播图的总容器,采用相对定位。imgCon为轮播图图片的容器,使用绝对定位,因为我们在切换图片时是在这个容器中增加DOM。
下面开始写JS代码

设定容器参数

<script>
  /*
    JS代码在这里
  */
  const config = {
    img: [
      "img/a.jpeg",
      "img/b.jpeg",
      "img/c.jpeg",
      "img/d.jpeg",
      "img/e.jpeg",
    ],
    leftClick: "img/left.png",
    rightClick: "img/right.png",
    autoPlay:true,
    autoPlayTime:300,
    time: 1500,
  };
  class Banner {
    constructor(config) {
      if (typeof config !== "object" || config === null) {
        throw Error("config  must be a object");
      }
      const { img, leftClick, rightClick, autoPlay, time, autoPlayTime } = config;
      this.img = img;
      this.leftClick = leftClick;
      this.rightClick = rightClick;
      this.autoPlay = autoPlay;
      this.time = time;
      this.getSize();
      this.createCacheImage();
    }

    getSize() {
      this.WIDTH = document.body.clientWidth;
      this.HEIGHT = this.WIDTH / 3;
    }

    createCacheImage() {
      this.bannerCache = this.img.map((src) => {
        const img = new Image();
        img.src = src;
        const style = {
          width: this.WIDTH + "px",
          height: this.HEIGHT + "px",
         };
        Banner.setStyle(img, style);
        return img;
      });
      this.btnCache = [this.leftClick, this.rightClick].map((src,index) => {
        const img = new Image();
        img.src = src;
        const className = index === 0 ? "left" : "right";
        img.type = className;
        img.setAttribute('class',`${className}_btn`)
        return img;
      });
    }
  }
  new Banner(config);
</script>
复制代码
  1. config为轮播图需要的参数,分别为轮播图URL,两侧按钮(也就是上一张/下一张)URL,是否为自动轮播以及切换图片所需要的时间。
  2. 我们把所有的配置参数都存到this中,随用随取。
  3. getSize为我设定的轮播图的尺寸,选这个尺寸的原因是因为我自己的图片比较大,全屏看着比较舒服,高度也是由图片来决定的,将宽高都存到this中,以后会用到。
  4. createCacheImage将轮播图片和按钮图片存到一个Map中,当我们切换下一张时可以直接偶从Map中取出来,而不用重新在创建图片DOM了,同时设置每张轮播图的样式。在btnCache循环中,我们把两个按钮通过设定type用以区分是左边还是右边,为了在点击事件中能够方便的区分;同时设为不同的类名,是为了样式能够定位到轮播图两侧(在这里css代码就不贴了。
class Banner {
 static addEvent = (type, callback, target = window) => {
  return target.addEventListener(type, callback);
 };

static setStyle = (element, style = {}) => {
  Object.assign(element.style, style);
  return element;
};

constructor(config) {
  if (typeof config !== "object" || config === null) {
    throw Error("config  must be a object");
  }
  const { img, leftClick, rightClick, autoPlay, time } = config;
  this.img = img;
  this.leftClick = leftClick;
  this.rightClick = rightClick;
  this.autoPlay = autoPlay;
+ Banner.addEvent("resize", this.updateSize.bind(this));
  this.time = time;
  this.getSize();
  this.createCacheImage();
+ this.setBannerSize();
}
//以下为新增、之前的函数方法不变

updateSize() {
  this.WIDTH = document.body.clientWidth;
  this.HEIGHT = this.WIDTH / 3;
}

setBannerSize() {
  if(!this.carousel){
   this.carousel = document.querySelector(".carousel");
  }
  const style = {
    width: this.WIDTH + 'px',
    height: this.HEIGHT + 'px'
  }
  Banner.setStyle(this.carousel, style);
  }
}
复制代码
  1. setStyleaddEvent是封装的公共的一个方法,分别是添加DOM样式和添加DOM事件,因为这两个方法通过函数传参就可以完成其功能,不需要获取当前this的任何信息,所以用static关键字写成了静态方法。
  2. window添加resize事件,是为了兼容窗口大小随时变化的可能,宽度变化时更新widthheight
  3. setBannerSizecarousel也就是轮播图的总容器设置宽高。

添加图片

class Banner {
// 静态方法省略
constructor(config) {
 // 省略之前的方法
 + this.appendImage();
 + this.appendBtn();
}
//以下为新增、之前的函数方法不变

appendImage(currentImage = 0) {
  if (typeof currentImage !== "number") {
    return;
  }
  if (!this.imgCon) {
    this.imgCon = document.querySelector(".imgCon");
    const style = {
      width:this.WIDTH + 'px',
      height: this.HEIGHT + 'PX',
    }
    this.setStyle(this.imgCon, style);
    this.carousel.appendChild(this.imgCon);
  }
  return this.imgCon.appendChild(this.bannerCache[currentImage]);
}

appendBtn() {
  this.btnCache.forEach((btn) => {
    Banner.addEvent("click", this.handleBtnClick.bind(this), btn);
    this.carousel.appendChild(btn);
  });
}

handleBtnClick(e){ 
 // 先省略
 }
}
复制代码
  1. appendImage将当前第N个图片添加到轮播图的父容器中,这里我们也是在HTML中写好了DOM,取出来直接存到this中,方便下次操作。这里我们参数currentImage表示要添加的第几个图片,因为之前我们已经将所有轮播图片缓存到了bannerCache中,所以可以通过索引从bannerCache中直接取出来进行添加,也是方便接下来我们若是点击轮播图下方的小圆点,可以通过其索引直接进行添加。
  2. appendBtn中省略handleBtnClick具体细节。

到了这一步我们可以先看一下我们的效果 好,基本上就是这个样式,但是这样有一个问题,就是我改变窗口大小的时候,轮播图容器的尺寸并没有发生相应的改变。 原因是我们虽然监听了window的resize事件,并且动态的修改了this.WIDTHthis.HEIGHT的值,但是DOM的尺寸并不会因为你手动修改了其值而动态的改变。(突然怀念起了框架的好处是怎么回事?) 接下来我们要监听一下WIDTHHEIGHT的变化。

使用Proxy监听

class Banner {
  // 以上省略
  static bindProxy = (object, callback, targetChangeCallback) => {
    if (typeof object !== "object" || object === null) return;
    const proxy = new Proxy(object, callback(targetChangeCallback));
    return proxy;
  };
  
  constructor(){
    // ...以上省略
    Banner.addEvent("resize", this.getSize.bind(this)); // 将resize回调函数设置getSize
    this.sizeInfo = {};
    this.sizeInfo = Banner.bindProxy(
    this.sizeInfo,
    this.sizeInfoChange,
    this.sizeInfoChangeCallback.bind(this)
    );
    this.getSize() //将this.getSize写到Proxy后边
  }
  
  getSize() {
    this.WIDTH = document.body.clientWidth;
    this.HEIGHT = this.WIDTH / 3;
    this.sizeInfo.width = this.WIDTH;
    this.sizeInfo.height = this.HEIGHT;
  }
  
/*此函数干掉,已经没有意义了
  updateSize() {
    this.WIDTH = document.body.clientWidth;
    this.HEIGHT = this.WIDTH / 3;
  }
)
*/  
  createCacheImage() {
    this.bannerCache = this.img.map((src) => {
      const img = new Image();
      img.src = src;
      /*
      const style = {
        width: this.WIDTH + "px",
        height: this.HEIGHT + "px",
      };
      */
      Banner.setStyle(img, this.sizeInfo);
      return img;
    });
    this.btnCache = [this.leftClick, this.rightClick].map((src) => {
      const img = new Image();
      img.src = src;
      return img;
    });
  }
  
  setBannerSize() {
    if(!this.carousel){
     this.carousel = document.querySelector(".carousel");
    }
    /*
    const style = {
      width: this.WIDTH + "px",
      height: this.HEIGHT + "px",
    };
    */
    Banner.setStyle(this.carousel, this.sizeInfo);
  }
  
  appendImage(currentImage = 0) {
    if (typeof currentImage !== "number") {
      return;
    }
    if (!this.imgCon) {
      this.imgCon = document.querySelector(".imgCon");
      /*
      Banner.setStyle(this.imgCon, {
        width: this.WIDTH + "px",
        height: this.HEIGHT + "px",
      });
      */
      Banner.setStyle(this.imgCon, this.sizeInfo);
      this.carousel.appendChild(this.imgCon);
    }
    return this.imgCon.appendChild(this.bannerCache[currentImage]);
  }
  
 sizeInfoChange(callback) {
  if (callback && typeof callback !== "function") {
    throw Error("callback must be a function!");
  }
  return {
    get: (target, key) => {
      return target[key];
    },
    set: (target, key, value) => {
      if (!["width", "height"].includes(key)) {
        throw Error("must be width or height");
      }
      if (typeof value !== "number") {
        throw Error("must be type of number");
      }
      const change = Reflect.set(target, key, value + "px");
      if (key === "height") {
        callback && callback();
      }
      return change;
    },
  };
}

handleBtnClick = () => {
  // 先省略
}

sizeInfoChangeCallback() {
  this.setBannerSize();
  this.bannerCache.forEach((img) => {
    Banner.setStyle(img, this.sizeInfo);
  });
 }
}
复制代码
  1. 简单科普一下Proxy的用法:

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2
复制代码

Object.defineProperty有异曲同工之妙,不过Proxy更优秀一些,在这里我不做详细阐述,掘金搜索Proxy有几篇不错的文章详细的讲述了Proxy的原理以及使用。
简单的说在这里我们就是在resize事件触发时,修改sizeInfo中width和height属性值,从而使回调函数sizeInfoChangeCallback触发,改变轮播图最外层容器的尺寸以及每张轮播图片的尺寸。
2. 将this.getSize移到Banner.bindProxy后边,是因为先对sizeInfo进行监听,然后通过getSize获取尺寸、修改尺寸,从而达到初次加载的时候设定容器以及图片的宽高。若是getSizeBanner.bindProxy之前,在监听sizeInfo之前尺寸就已经有了,而我们把所有用到widthheight的地方全部改成了sizeInfo,那么初次加载就拿不到widthheight的信息了。
现在容器和图片的尺寸会随着屏幕的变化而变化了

无缝轮播

接下来是轮播图的核心功能:无缝轮播。当我们点击左右按钮时,会无缝切换到上一张/下一张。
当点击左按钮时,在当前图片的前边增加其上一张图片,成功后轮播图的容器向右滑动。 点击右按钮则反过来。

新增节点

接下来我们完成之前空置的按钮事件

class Banner{
  constructor(config){
    // ...以上所有代码不变
    this.currentImage = {
     index: 0, //当前显示的图片
    };
    this.currentImage = Banner.bindProxy(
      this.currentImage,
      this.currentImageChange,
      this.sizeInfoChangeCallback.bind(this)
    );
    this.isPlaying = false; //是否正在移动
  }
  
  handleBtnClick(){
    if (this.isPlaying) return;
    e.preventDefault();
    e.stopPropagation();
    const { type } = e.target;
    let currentImage;
    this.direction = type;
    if (type === "left") {
      currentImage = this.currentImage.index - 1;
      if (currentImage < 0) {
        currentImage = this.bannerCache.length - 1;
      }
      return (this.currentImage.index = currentImage);
    }
    if (type === "right") {
      currentImage = this.currentImage.index + 1;
      if (currentImage >= this.bannerCache.length) {
        currentImage = 0;
      }
      return (this.currentImage.index = currentImage);
    }
  }
  
  currentImageChange(callback){
    if (callback && typeof callback !== "function") {
      throw Error("callback must be a function!");
    }
    return {
      get: (target, key) => {
        return target[key];
      },
      set: (target, key, value) => {
        if (key !== "index") {
          throw Error("you can set `index` property at currentImage");
         }
         if (typeof value !== "number") {
          throw Error("must be type of number");
        }
        const change = Reflect.set(target, key, value);
        callback && callback();
        return change;
      },
    };
  }
  
  currentImageChangeCallback() {
    const type = this.direction;
    this.imgCon.style.width = this.WIDTH * 2 + "px";
    if (type === "left") {
      this.imgCon.insertBefore(
        this.bannerCache[this.currentImage.index],
        this.imgCon.firstElementChild
      );
      this.imgCon.style.left = -this.WIDTH + "px";
    } else {
      this.appendImage(this.currentImage.index);
    }
    this.isPlaying = true;
  }
}
复制代码
  1. 先看this.currentImage,因为我们切换图片是要切换当前index,也就是切换bannerCache的索引,所以当index改变时我们进行操作,跟监听宽高变化时一样,使用Proxy进行监听当前图片索引的变化。
  2. this.isPlaying表明当前是否是在轮播切换中,若是则阻止一切操作,因为若切换图片时连续点击按钮,频繁操作会影响交互。
  3. handleBtnClick中的代码,我们之前为左右按钮增加了type属性,此时就用到了,若是点击的左侧按钮,先将此时点击的方向存到this.direction中,接下来会用到。点击左侧按钮代表切换上一张图片,假设当前索引是2,也就是显示的第3张图片,那么就应该将第2张图片添加到当前图片前边,进行右滑,点击右侧按钮相反。当前这里需要注意边界条件,就是第0张和最后一张。
  4. 点击哪个按钮应该切换到第几张图我们拿到这个结果后,赋值给currentImageindex属性,Proxy中回调函数currentImageChangeCallback触发。
  5. 因为不管是左滑还是右滑,都会在当前轮播图容器中增加一张图,所以需要先设置当前容器width为原来的2倍,然后在根据之前存的direction来判断是向前增加图片还是向后增加图片。
  6. 有一个样式问题需要注意,若是点击左侧按钮,需要向当前图片节点之前增加一个节点,若是增加成功,新增的节点会直接将当前节点挤到后边去,取而代之显示的就是新增的节点了,我们要手动处理left的距离,然后通过动画将上述行为展现出来。

我们分别点击一下左右侧的按钮:
点击左侧按钮:在当前节点之前增加
点击右侧按钮:在当前节点之后增加

requestAnimationFrame动画过渡

class Banner{
  constructor(config){
  	//...省略以上代码
    this.FPS = 60;
    this.getFPS();
    this.computeSpeed()
    this.animation();
  }
  
  getFPS() {
    let now = Date.now();
    let lastNow = Date.now();
    let count = 0;
    let countTotal = 0;
    const countArr = [];
    let computeFrame = null;
    const compute = () => {
      now = Date.now();
      count++;
      computeFrame = requestAnimationFrame(compute);
      if (now - lastNow >= 1000) {
        lastNow = Date.now();
        const length = countArr.push(count);
        countTotal = countTotal + count;
        count = 0;
        if (length === 5) {
          this.FPS = Math.round(countTotal / 5);
          cancelAnimationFrame(computeFrame);
        }
      }
    };
    compute();
  }
  
  computeSpeed(){
    const rCount = 1000 / this.FPS;
    this.speed = this.WIDTH / (this.time / rCount);
  }
  
  animation() {
    this.callback = requestAnimationFrame(this.animation.bind(this));
    this.carouselPlaying();
  }
  
  carouselPlaying() {
    if (!this.isPlaying) return;
    const direction = this.direction;
    const imgCon = this.imgCon;
    if (direction === "left") {
      imgCon.style.left = imgCon.offsetLeft + this.speed + "px";
      if (imgCon.offsetLeft >= 0) {
        this.isPlaying = false;
        imgCon.lastElementChild.remove();
        imgCon.style.left = "0px";
      }
    } else if (direction === "right") {
      imgCon.style.left = imgCon.offsetLeft - speed + "px";
      if (imgCon.offsetLeft <= -this.WIDTH) {
        this.isPlaying = false;
        imgCon.firstElementChild.remove();
        imgCon.style.left = "0px";
      }
    }
  }
}
复制代码

在我们新增了一个节点后,需要让当前轮播图的容器进行左/右移动,使新增的节点显现出来,这里我们使用requestAnimationFrameAPI。它在MDN的解释:window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
也就是浏览器每次重绘来调用其回调函数,重绘频率为屏幕的刷新率,简称FPS,也就是每秒屏幕刷新的次数。
一般我们的屏幕刷新频率为60HZ,也就是每秒刷新60次,平均16.7毫秒刷新一次,但是我们不知道轮播图真正跑在多HZ的屏幕上,所以这里需要计算一下
我们在初始的config参数中传了time = 1500ms,所以我们要在1500ms内完成轮播的动作,那么计算:

  1. 假设屏幕刷新率为N,刷新一次需要的毫秒数 = 1000/每秒刷新的次数(N)
  2. 1500毫秒完成动作,需要刷新的次数 = 1500 / 刷新一次需要的毫秒数
  3. 所以 每一次刷新需要移动的距离 = 总共需要移动的总距离 / 需要刷新的次数

computeSpeed在设定屏幕刷新率为60HZ情况下计算每一次刷新的速度,而与此同时屏幕真正的FPS也在计算中,通过getFPS函数中compute函数每一秒运行的次数,取5次的平均值就可以得出准确的FPS,得出FPS后在从新计算speed
在初始化的时候就运行animation函数,在内部使用requestAnimationFrame回调本身和carouselPlaying,若是点击左/右按钮之一,this.isPlaying值设为truecarouselPlayingthis.isPlaying一旦为true,后续的代码就能在每一帧中被运行,通过判断direction是进行向左还是向右移动,进行移动,carouselPlaying每16.7ms运行一次,直到将下个要展示的图片拉倒当前位置,然后销毁上一张,整个轮播过程就完成了。 注意:移动的过程我们用的是offsetLeft,也就是轮播图父容器边缘距离总容器的距离,计算出需要移动的距离,然后改变轮播图父容器的left值。下图可以清晰的表达出offsetLeft的意义 我们来看一下现在的轮播效果

圆点跳播

有没有觉得这个轮播图缺了一点东西?嗯?没错,就是轮播图下边的小圆点,没有圆点进度,只能一个一个的播放,若是从第一张直接跳第三张呢?接下里我们开始增加小圆点。
html里增加一个圆点的父容器,然后设定好其样式

<style>
  * {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
  }
  .carousel {
    position: relative;
    margin: auto;
    overflow: hidden;
    margin: 30px auto;
  }
  .imgCon {
    position: absolute;
    left: 0;
    right: 0;
  }
  .left,
  .right {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
  }
  .left {
    left: 10px;
  }
  .right {
    right: 10px;
  }
  /* 圆点父容器 */
  .processDot {
    position: absolute;
    bottom: 20px;
    width: 200px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    justify-content: space-around;
    z-index: 2;
  }
  /* 每个小圆点的样式 */
  .process_dot {
    list-style: none;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: 1px solid red;
  }
  /* 当前轮播图片索引和圆点索引一致时 */
  .process_dot[active="true"] {
    background-color: red;
  }
</style>
<div class="carousel">
  <!-- 轮播图的最外层的容器 -->
  <div class="imgCon"></div>
  <!-- 轮播图图片的容器-->
  <ul class="processDot"></ul>
</div>
复制代码
class Banner{
  constructor(){
    // ...省略以上所有
    this.appendDot();
  }
  
  handleDotClick(index, e) {
    e.preventDefault();
    e.stopPropagation();
    if (this.isPlaying) return;
    if (index > this.currentImage.index) {
      this.direction = "right";
    }
    if (index < this.currentImage.index) {
      this.direction = "left";
    }
    this.currentImage.index = index;
  }
  
  appendDot() {
    this.processDot = document.querySelector(".processDot");
    const fragment = document.createDocumentFragment();
    this.dotCache = this.bannerCache.map((img, index) => {
      const dot = document.createElement("li");
      dot.setAttribute("class", "process_dot");
      if (index === this.currentImage.index) {
        dot.setAttribute("active", true);
      }
      Banner.addEvent(
        "click",
        this.handleDotClick.bind(this, index),
        dot
      );
      fragment.appendChild(dot);
      return dot;
    });
    this.processDot.appendChild(fragment);
  }
  
  currentImageChangeCallback() {
    const type = this.direction;
    this.imgCon.style.width = this.WIDTH * 2 + "px";
    if (type === "left") {
      this.imgCon.insertBefore(
        this.bannerCache[this.currentImage.index],
        this.imgCon.firstElementChild
      );
      this.imgCon.style.left = -this.WIDTH + "px";
    } else {
      this.appendImage(this.currentImage.index);
    }
    const index = this.currentImage.index;
   + this.dotCache.forEach((dot, dotIndex) => {
   +  if (index === dotIndex) {
   +     return dot.setAttribute("active", true);
   +  }
   +   dot.setAttribute("active", false);
   + });
    this.isPlaying = true;
  }
}
复制代码
  1. 根据轮播图数量创建小圆点,把当前正在轮播的图片索引对应的小圆点设置active属性,并为每一个小圆点增加点击事件
  2. 点击小圆点时,判断每个小圆点的索引与当前轮播图的索引关系,找出应该轮播的方向,并将索引赋值给要轮播到的图片。
  3. currentImage.index回调函数中增加小圆点索引与当前轮播索引的判断,若相等,则activetrue,否则为false。

来看一下效果: 目前为止我们的轮播图就已经完成了85%,那么还差什么呢?没错,自动轮播!当页面停留&鼠标在轮播图之外我们进行自动轮播,为什么呢?因为鼠标一旦hover在轮播图上面,大概率用户要进行某些轮播操作,比如点击事件,查看大图事件...

自动轮播

class Banner{
  static removeEvent = (type, callback, target = window) => {
    return target.removeEventListener(type, callback);
  };
  constructor(){
    //... 省略
    + this.autoPlayTime = autoPlayTime/this.rCount;
    + this.cuteTime = this.autoPlayTime;
    + this.autoPlayEvent();
    + this.addMouseEvent();
  }
  
  autoPlayEvent() {
    if (this.autoPlay) {
      this.autoPlaying = true;
    }
  }
  
  addMouseEvent() {
    if (this.autoPlay) {
      Banner.addEvent(
        "mouseenter",
        this.handleMouse.bind(this),
        this.carousel
      );
      Banner.addEvent(
        "mouseleave",
        this.handleMouse.bind(this),
        this.carousel
      );
    }
  }
  
  handleMouse(e) {
    if (e.type === "mouseenter") {
      this.autoPlaying = false;
    }
    if (e.type === "mouseleave") {
      this.autoPlaying = true;
    }
  }
  
  computeSpeed() {
    //const rCount = 1000 / this.FPS;
    this.rCount = 1000 / this.FPS;
    this.speed = this.WIDTH / (this.time / this.rCount);
  }
  
  animation() {
    this.callback = requestAnimationFrame(this.animation.bind(this));
    this.carouselPlaying();
  + this.handleAutoPlay();
  }
  
  handleAutoPlay() {
    if (!this.autoPlaying) return;
    this.cuteTime--;
    if (this.cuteTime > 0) return;
    this.cuteTime = this.autoPlayTime;
    var evt = new MouseEvent("click");
    this.btnCache[1].dispatchEvent(evt);
  }
}
复制代码
  1. 先看新增的autoPlayEvent函数,判断autoPlay是都需要自动轮播,若是,开启自动轮播的开关autoPlaying
  2. animation函数中新增了一个回调函数handleAutoPlay,就是让图片进行自动轮播使用,内部逻辑是判断自动轮播开关autoPlaying是否已经开启,若开启,按照传入的时间,隔那个时间轮播一次
  3. 因为autoPlayTime是用户想要隔多少秒需要进行一次轮播,而我们的关键帧运动是通过计算得出的,若是60PFS就是每16.7ms执行一次handleAutoPlay,而this.cuteTime每次只减1,所以需要计算一下达到用户想要的那个轮播时间。
  4. 自动轮播我们使用的是自定义事件,因为左右两侧的按钮已经有了点击事件来进行轮播,所以我们只要通过触发右侧按钮点击事件就可以进行轮播了。
  5. 为轮播图整体添加鼠标进入和移除事件,因为鼠标一旦hover在轮播图上,那么用户的目的很大概率是要进行点击事件,这时候就应该停止轮播了。

最后,这个轮播图就已经完成了,我们来看一下最终的效果 ohshit!gif图片太大了掘金装不下。。
代码地址先献上

总结

总结一下轮播图中的各种细节:

  1. 使用Proxy代替传统的回调函数,通过监听数据的改变来执行事件,可以使代码更好的解耦。
  2. requestAnimationFrame代替传统的setTimeout,理论上可以使动画更加丝滑,并且节省性能。
  3. 缓存图片值,每次轮播不要重新再去请求图片,侧面也节省了性能。
  4. 考虑边界条件,比如轮播过程中需要禁止轮播事件再次执行,鼠标hover禁止自动轮播。

在写作过程中,笔者还想到在创建图片的时候其实应该用Promise,轮播图片全部加载成功整个轮播图才可以创建,因为图片加载时异步的,若是第一张图片加载时间过长那么就会发生JS代码错误,其中某个图片加载失败也要考虑相应的处理办法~。

文章分类
前端
文章标签