【思考】- 如何使用面相对象的方式来设计一个轮播图

前言

和不同的编程语言对比,JavaScript 是相对灵活的

Java (纯面向对象),Golang(函数式)这些语言不同,JavaScript 作为一门非常灵活的编程语言:

它的用法包括且不限于:

  • 函数式编程(闭包,回调,柯里化,惰性函数,记忆函数,偏函数...),
  • 面向对象编程(构造函数,类,设计模式)...

在不同的应用场景和问题面前,函数式编程和面相对象编程的选择曾经困扰了许许多多的前端工程师。 但是仔细对比之下,函数式编程和面相对象还是非常不同的:

函数式编程?面向对象编程?

关于函数式编程

函数式编程指的是以每个函数作为最小的编程单位,通过调用不同的函数来实现相对应的功能。

  • 既然将每个函数作为最小编程单位,那么每个函数之间只会存在相互调用,而不会过度依赖甚至污染其他的函数(参考 纯函数
  • 函数式编程的侧重点是功能的实现,最终每一个函数的调用都会去实现相对应的功能
  • 函数式编程由于写起来十分轻巧广受现在的前端项目的青睐 --- 因为前端交互的一个重要目的都是为了渲染视图,而函数的偏向点更利于快节奏,轻巧的视图渲染(e.g 创建视图的函数,接收视图渲染的函数)
  • 函数式编程的集成性较弱,因为每个函数之间不会过分的依赖

关于面相对象编程

相比于函数式编程,面向对象编程的侧重点则完全不同 - 可以理解成把每一个构造函数和类理解成是一个创建产品的机器或者工厂,每一个工厂可以接收不同的参数来创建不同的产品实例,每个产品又有自己的特点(属性)和功能(方法)

相比之下,面向对象编程的主要考虑点在于:

  • 每需要一个产品都可以通过构造函数和类进行实例化创建(复用性)
  • 属性,方法之间(属性与方法,方法与方法)的相互依赖和调用(集成性)
  • 权限修饰和控制外界访问内容(封装性)
  • 不同的类之间可以相互继承(继承性)
  • 一个类可以调用另外一个类的实例进行操作(实例观察)
  • 不同的子类都可以以父类的形式进行调用和操作(多态性)
  • 扩展性相比函数式编程更好(类比在工厂添加一个车间 😂)
  • ...

如何进行取舍?

  • 如果需要实现特定的功能,那么优先函数式编程
  • 如果需要考虑到复用、封装、集成,那么一般会考虑面相对象编程

轮播图使用面相对象来实现的原因:

  1. 复用:在不同的 DOM 节点上,可以通过配置实例化的方式来实现配置和实现不同的轮播图
  2. 集成:轮播图的种类有很多(淡入淡出(fade), 无缝滑动(slide), 3d轮播(slide3d)...) 对于它们来说, 都可以实现一个大型的工厂来进行管理
  3. 扩展:每次实现一个类型的轮播图,直接继承轮播图的父类,实现 / 重写方法即可

代码实现

说明

前端的程序设计通常都是结构、样式、逻辑分离的,接下来本文会将不同类型的轮播图按照结构、样式、逻辑进行说明

不同类型的轮播图:

实现一个淡入淡出的轮播图

结构

直接按照轮播图的结构进行布局即可(这里用到的图片是淘宝官网上面的)

html 复制代码
<div class="carousel J_Carousel fade">
    <div class="carousel-inner">
      <div class="carousel-item active">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i2/6000000001650/O1CN01fonnlj1O3kORSHmOY_!!6000000001650-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i4/6000000007762/O1CN01o1nsa027D39FXEK4o_!!6000000007762-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i1/6000000005444/O1CN01nzZtay1q5P44gB6Os_!!6000000005444-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i2/6000000007960/O1CN01K54n3N28fjZNIvtuY_!!6000000007960-2-octopus.png" />
        </a>
      </div>
    </div>
    <p class="carousel-item-text"></p>
    <ul class="dot-list">
      <li class="dot-list-item active"></li>
      <li class="dot-list-item"></li>
      <li class="dot-list-item"></li>
      <li class="dot-list-item"></li>
    </ul>
    <div class="director-wrap">
      <button class="director director-left"></button>
      <button class="director director-right"></button>
    </div>
</div>

样式

本文使用的是 sass 对 html 结构进行布局

scss 复制代码
html,
body {
  margin: 0;
  padding: 0;
  font-size: 15px;
  color: #3d3d3d;
}

div,
p,
ul,
li,
ol {
  margin: 0;
  padding: 0;
}

ul,
li,
ol,
a {
  text-decoration: none;
  list-style: none;
}

.carousel {
    width: 520px;
    height: 260px;
    margin: 20px auto;
    
    .fade {
        position: relative;
    
        .carousel-inner {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;

            .carousel-item {
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                opacity: 0;
                transition: all .25s ease-in;

                &.active {
                    opacity: 1;
                }

                img {
                    display: block;
                    width: 100%;
                    height: 100%;
                }
            }
        }
    }
}

逻辑

  1. 程序从外往里设计,src/app.js中需要一个 CarouselFade 类用来处理淡入淡出轮播
js 复制代码
/* src/app.js */

import { CarouselFade } from './carousel';

new CarouselFade('.J_Carousel', {
  autoplay: true,
  duration: 1500
});
  1. carousel/index.js 中导出 CarouselFade
javascript 复制代码
import CarouselFade from './CarouselFade';

export {
  CarouselFade
};
  1. 编写一下 CarouselFade 这个类
js 复制代码
class CarouselFade {
  constructor(selector, options) {
    // 根元素
    this.el = document.querySelector(selector);
    // 各一张轮播图组成的元素列表
    this.carouselItemList = [...this.el.querySelectorAll('.carousel-item')];
    // 小圆点列表容器
    this.dotWrap = this.el.querySelector('.dot-list');
    // 小圆点列表
    this.dotItemList = [...this.el.querySelectorAll('.dot-list-item')];
    // 指示器容器
    this.director = this.el.querySelector('.director-wrap');

    this.autoplay = !!options.autoplay;
    this.duration = options.duration || 2000;

    this.timer = null;
    this.currentIndex = 0;

    this.init();
  }

  init() {
    this.initContainer();
    this.bindEvent();
    this.autoplay && this.run();
  }

  initContainer() {
    this.el.style.display = 'none';
    const elClassList = this.el.className.split(' ');

    if (!elClassList.includes('fade')) {
      this.el.classList.add('fade');
    }
    this.el.style.display = '';
  }

  bindEvent() {
    this.el.addEventListener('mouseenter', this.handleMouseEnterLeave.bind(this), false);
    this.el.addEventListener('mouseleave', this.handleMouseEnterLeave.bind(this), false);
    this.director.addEventListener('click', this.handleDirectorClick.bind(this), false);
    this.dotWrap.addEventListener('click', this.handleDotClick.bind(this), false);
  }

  run() {
    this.timer = setInterval(() => {
      this.changeCaourselByDir('next');
    }, this.duration);
  }

  changeCaourselByDir(dir) {
    switch (dir) {
      case 'next':
      this.currentIndex = this.currentIndex === this.carouselItemList.length - 1
                        ? 0
                        : this.currentIndex + 1;
        break;
      case 'prev':
      this.currentIndex = this.currentIndex === 0
                        ? this.carouselItemList.length - 1
                        : this.currentIndex - 1;
        break;
      default:
        break;
    }
    this.changeCarouselDOM(this.currentIndex);
  }

  changeCarouselDOM(currentIndex) {
    this.carouselItemList.forEach((el, elIdx) => {
      el.className = 'carousel-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
    this.dotItemList.forEach((el, elIdx) => {
      el.className = 'dot-list-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
  }

  handleMouseEnterLeave(ev) {
    const e = ev || window.event;
    const evType = e.type;

    switch (evType) {
      case 'mouseenter':
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null;
        }
        break;
      case 'mouseleave':
        if (!this.timer) {
          Promise.resolve().then(() => {
            this.autoplay && this.run();
          });
        }
        break;
      default:
        break;
    }
  }

  handleDirectorClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('director-left')) {
      this.changeCaourselByDir('prev');
    }
    if (elClassList.includes('director-right')) {
      this.changeCaourselByDir('next');
    }
  }

  handleDotClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('dot-list-item')) {
      const currentIndex = this.dotItemList.findIndex(item => item === el);
      this.currentIndex = currentIndex;
      this.changeCarouselDOM(this.currentIndex);
    }
  }
}

export default CarouselFade;

实现一个无缝滑动的轮播图

结构

结构除了把第一行的 fade 类去掉,其他和上面是一样的

html 复制代码
<div class="carousel J_Carousel">
    <div class="carousel-inner">
      <div class="carousel-item active">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i2/6000000001650/O1CN01fonnlj1O3kORSHmOY_!!6000000001650-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i4/6000000007762/O1CN01o1nsa027D39FXEK4o_!!6000000007762-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i1/6000000005444/O1CN01nzZtay1q5P44gB6Os_!!6000000005444-0-octopus.jpg" />
        </a>
      </div>
      <div class="carousel-item">
        <a href="javascript:;">
          <img alt="" src="https://img.alicdn.com/imgextra/i2/6000000007960/O1CN01K54n3N28fjZNIvtuY_!!6000000007960-2-octopus.png" />
        </a>
      </div>
    </div>
    <p class="carousel-item-text"></p>
    <ul class="dot-list">
      <li class="dot-list-item active"></li>
      <li class="dot-list-item"></li>
      <li class="dot-list-item"></li>
      <li class="dot-list-item"></li>
    </ul>
    <div class="director-wrap">
      <button class="director director-left"></button>
      <button class="director director-right"></button>
    </div>
</div>

样式

样式如下:

scss 复制代码
html,
body {
  margin: 0;
  padding: 0;
  font-size: 15px;
  color: #3d3d3d;
}

div,
p,
ul,
li,
ol {
  margin: 0;
  padding: 0;
}

ul,
li,
ol,
a {
  text-decoration: none;
  list-style: none;
}

.carousel {
  width: 520px;
  height: 260px;
  margin: 20px auto;

  &.slide {
    position: relative;
    transition: all .25s ease-in-out;
    overflow-x: hidden;

    .carousel-inner {
      width: 100%;
      height: 100%;
      transition: all .25s ease-in-out;
      position: relative;

      &:after {
        content: "";
        display: table;
        clear: both;
      }

      .carousel-item {
        float: left;
        width: 520px;
        height: 100%;

        img {
          display: block;
          width: 100%;
          height: 100%;
        }
      }
    }
  }

  .carousel-item-text {
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    height: 36px;
    line-height: 36px;

    &.show {
      display: block;
    }
  }

  .dot-list {
    position: absolute;
    bottom: 0;
    right: 0;
    width: 100%;
    padding: 0 5px 8px 0;
    box-sizing: border-box;
    text-align: right;

    .dot-list-item {
      padding: 0;
      display: inline-block;
      color: #fff;
      background-color: #fff;
      border-radius: 50%;
      margin-right: 5px;
      width: 10px;
      height: 10px;
      cursor: pointer;
      transition: all .25s ease-in;

      &.active {
        background-color: #ff5000;
        color: #ff5000;
      }
    }
  }

  .director-wrap {
    position: absolute;
    top: 50%;
    left: 0;
    width: 100%;
    height: 44px;
    margin-top: -22px;

    .director {
      position: absolute;
      top: 50%;
      height: 36px;
      margin-top: -22px;
      width: 36px;
      background-color: rgba(0, 0, 0, .4);
      color: #d2d2d2;
      border: none;
      border-radius: 5px;
      padding: 5px;
      box-sizing: border-box;
      cursor: pointer;
      transition: all .25s ease-in;

      &:hover {
        background-color: rgba(0, 0, 0, .6);
        color: #fff;
      }

      &-left {
        left: 0;

        &::after {
          content: "<";
        }
      }

      &-right {
        right: 0;
        &::after {
          content: ">";
        }
      }
    }
  }
}

逻辑

  1. 首先程序从外往里设计,先在 src/app.js 中引入
js 复制代码
import { CarouselSlide } from './carousel';

new CarouselSlide('.J_Carousel', {
  autoplay: true,
  duration: 1500
});
  1. carousel/index.js 中追加导出 CarouselSlide

  2. 代码实现

注意

和淡入淡出的轮播图不同,无缝轮播在切换视图时会有【临界点】的问题:

  • 如果方向为正(next),需要从最后一张图片滚动到第一张图片
  • 如果方向为反(prev),需要从第一张图滚动到最后一张图

如果直接设置 transform: translateX(xxx), 轮播图的表现将十分的怪异(e.g 反向从最后一张走回第一张)。这个时候,我们就需要使用一些视觉欺骗(用户无感知操作)了。

本文的解决方案是这样的:

  1. 初始化模块的时候,深克隆第一个 CarouselItem 元素(el.cloneNode(true)), 并把这个元素放到 CarouselList 的最后面
  2. 如果轮播图滑动的方向为正,并且已经滑动到了最后一张,处理逻辑:让最后一张滑动到克隆的图片上,并且在滑动完成后瞬间将克隆的图片的位置无过渡地移动回到第一张
  3. 如果轮播图的方向为负,并且轮播图需要从第一张【回到】最后一张,处理逻辑:让第一张图片瞬间回到克隆第一张元素的位置,然后再从克隆的图片滚动【回到】最后一张图片
js 复制代码
class CarouselSlide {
  constructor(selector, options) {
    this.el = document.querySelector(selector);
    this.elInner = this.el.querySelector('.carousel-inner');
    this.carouselItemList = [...this.el.querySelectorAll('.carousel-item')];
    this.dotWrap = this.el.querySelector('.dot-list');
    this.dotItemList = [...this.el.querySelectorAll('.dot-list-item')];
    this.director = this.el.querySelector('.director-wrap');

    this.autoplay = !!options.autoplay;
    this.duration = options.duration || 2000;

    this.timer = null;
    this.currentIndex = 0;
    this.len = this.carouselItemList.length;

    this.init();
  }

  init() {
    this.cloneNode();
    this.initContainer();
    this.bindEvent();
    this.autoplay && this.run();
  }

  cloneNode() {
    const { carouselItemList, elInner } = this;
    const cloneItem = carouselItemList[0].cloneNode(true);
    elInner.appendChild(cloneItem);
  }

  initContainer() {
    const elClassList = this.el.className.split(' ');

    if (!elClassList.includes('slide')) {
      this.el.classList.add('slide');
    }
    this.elInner.style.cssText = `
      width: ${(this.carouselItemList.length + 1) * 100 + '%'};
      transform: translateX(0);
    `;
  }

  bindEvent() {
    this.el.addEventListener('mouseenter', this.handleMouseEnterLeave.bind(this), false);
    this.el.addEventListener('mouseleave', this.handleMouseEnterLeave.bind(this), false);
    this.director.addEventListener('click', this.handleDirectorClick.bind(this), false);
    this.dotWrap.addEventListener('click', this.handleDotClick.bind(this), false);
  }

  handleMouseEnterLeave(ev) {
    const e = ev || window.event;
    const evType = e.type;

    switch (evType) {
      case 'mouseenter':
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null;
        }
        break;
      case 'mouseleave':
        if (!this.timer) {
          Promise.resolve().then(() => {
            this.autoplay && this.run();
          });
        }
        break;
      default:
        break;
    }
  }

  handleDirectorClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('director-left')) {
      this.changeCaourselByDir('prev');
    }
    if (elClassList.includes('director-right')) {
      this.changeCaourselByDir('next');
    }
  }

  handleDotClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('dot-list-item')) {
      const currentIndex = this.dotItemList.findIndex(item => item === el);
      const prevIdx = this.currentIndex;
      this.currentIndex = currentIndex;

      const dir = prevIdx > currentIndex ? 'prev' : 'next';

      this.changeCarouselDOM(this.currentIndex, dir);
    }
  }

  run() {
    this.timer = setInterval(() => {
      this.changeCaourselByDir('next');
    }, this.duration);
  }

  changeCaourselByDir(dir) {
    switch (dir) {
      case 'next':
      this.currentIndex = this.currentIndex === this.carouselItemList.length - 1
                        ? 0
                        : this.currentIndex + 1;
        break;
      case 'prev':
      this.currentIndex = this.currentIndex === 0
                        ? this.carouselItemList.length - 1
                        : this.currentIndex - 1;
        break;
      default:
        break;
    }
    this.changeCarouselDOM(this.currentIndex, dir);
  }

  changeCarouselDOM(currentIndex, dir) {
    const elWidth = parseInt(window.getComputedStyle(this.el).width);

    if (dir === 'next') {
      if (currentIndex === 0) {
        this.elInner.style.transform = `translateX(-${this.len * elWidth}px)`;
        setTimeout(() => {
          const { transition } = this.elInner.style;
          this.elInner.style.transition = 'none';
          this.elInner.style.transform = `translateX(0)`;
          setTimeout(() => {
            this.elInner.style.transition = transition;
          }, 200);
        }, 200);
      } else {
        this.elInner.style.transform = `translateX(-${currentIndex * elWidth}px)`;
      }
    }
    if (dir === 'prev') {
      if (currentIndex === this.len - 1) {
        const { transition } = this.elInner.style;
        this.elInner.style.transition = 'none';
        this.elInner.style.transform = `translateX(-${this.len * elWidth}px`;
        setTimeout(() => {
          this.elInner.style.transition = transition;
          this.elInner.style.transform = `translateX(-${(this.len - 1) * elWidth}px)`;
        }, 200);
      } else {
        this.elInner.style.transform = `translateX(-${currentIndex * elWidth}px)`;
      }
    }

    this.dotItemList.forEach((el, elIdx) => {
      el.className = 'dot-list-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
  }
}

export default CarouselSlide;

像这样,无缝滑动的轮播图也处理好了:

轮播图模块的集成

上面我们实现了 CarouselFadeCarouselSlide 这两个模块,但是还是存在一些问题:

  • 两个模块的代码冗余
  • 没有集成起来
  1. 我们把CarouselFadeCarouselSlide提取到父类 Carousel
js 复制代码
export default class Carousel {
  constructor(selector, options) {
    this.el = document.querySelector(selector);
    this.elInner = this.el.querySelector('.carousel-inner');
    this.carouselItemList = [...this.el.querySelectorAll('.carousel-item')];
    this.dotWrap = this.el.querySelector('.dot-list');
    this.dotItemList = [...this.el.querySelectorAll('.dot-list-item')];
    this.director = this.el.querySelector('.director-wrap');

    this.autoplay = !!options.autoplay;
    this.duration = options.duration || 2000;

    this.timer = null;
    this.currentIndex = 0;
    this.len = this.carouselItemList.length;

    this.init();
  }

  init() {
    this.initContainer();
    this.bindEvent();
    this.autoplay && this.run();
  }

  initContainer() {
    throw new Error('initContainer must be implemented');
  }

  bindEvent() {
    this.el.addEventListener('mouseenter', this.handleMouseEnterLeave.bind(this), false);
    this.el.addEventListener('mouseleave', this.handleMouseEnterLeave.bind(this), false);
    this.director.addEventListener('click', this.handleDirectorClick.bind(this), false);
    this.dotWrap.addEventListener('click', this.handleDotClick.bind(this), false);
  }

  handleMouseEnterLeave(ev) {
    const e = ev || window.event;
    const evType = e.type;

    switch (evType) {
      case 'mouseenter':
        if (this.timer) {
          clearInterval(this.timer);
          this.timer = null;
        }
        break;
      case 'mouseleave':
        if (!this.timer) {
          Promise.resolve().then(() => {
            this.autoplay && this.run();
          });
        }
        break;
      default:
        break;
    }
  }

  handleDirectorClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('director-left')) {
      this.changeCaourselByDir('prev');
    }
    if (elClassList.includes('director-right')) {
      this.changeCaourselByDir('next');
    }
  }

  handleDotClick(ev) {
    const e = ev || window.event;
    const el = e.target || e.srcElement;
    const elClassList = el.className.split(' ');

    if (elClassList.includes('dot-list-item')) {
      const currentIndex = this.dotItemList.findIndex(item => item === el);
      const prevIdx = this.currentIndex;
      this.currentIndex = currentIndex;

      const dir = prevIdx > currentIndex ? 'prev' : 'next';

      this.changeCarouselDOM(this.currentIndex, dir);
    }
  }

  run() {
    this.timer = setInterval(() => {
      this.changeCaourselByDir('next');
    }, this.duration);
  }

  changeCaourselByDir(dir) {
    switch (dir) {
      case 'next':
      this.currentIndex = this.currentIndex === this.carouselItemList.length - 1
                        ? 0
                        : this.currentIndex + 1;
        break;
      case 'prev':
      this.currentIndex = this.currentIndex === 0
                        ? this.carouselItemList.length - 1
                        : this.currentIndex - 1;
        break;
      default:
        break;
    }
    this.changeCarouselDOM(this.currentIndex, dir);
  }

  changeCarouselDOM(currentIndex, dir) {
    throw new Error('changeCarouselDOM must be implemented');
  }
}
  1. 改写 CarouselFadeCarouselSlide
js 复制代码
/* CarouselFade */
import Carousel from './Carousel';

class CarouselFade extends Carousel {
  constructor(selector, options) {
    super(selector, options);
  }

  initContainer() {
    this.el.style.display = 'none';
    const elClassList = this.el.className.split(' ');

    if (!elClassList.includes('fade')) {
      this.el.classList.add('fade');
    }
    this.el.style.display = '';
  }

  changeCarouselDOM(currentIndex) {
    this.carouselItemList.forEach((el, elIdx) => {
      el.className = 'carousel-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
    this.dotItemList.forEach((el, elIdx) => {
      el.className = 'dot-list-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
  }
}

export default CarouselFade;
js 复制代码
/* CarouselSlide */
import Carousel from './Carousel';

class CarouselSlide extends Carousel {
  constructor(selector, options) {
    super(selector, options);
  }

  init() {
    this.cloneNode();
    super.init();
  }

  cloneNode() {
    const { carouselItemList, elInner } = this;
    const cloneItem = carouselItemList[0].cloneNode(true);
    elInner.appendChild(cloneItem);
  }

  initContainer() {
    const elClassList = this.el.className.split(' ');

    if (!elClassList.includes('slide')) {
      this.el.classList.add('slide');
    }
    this.elInner.style.cssText = `
      width: ${(this.carouselItemList.length + 1) * 100 + '%'};
      transform: translateX(0);
    `;
  }

  changeCarouselDOM(currentIndex, dir) {
    const elWidth = parseInt(window.getComputedStyle(this.el).width);
    
    if (dir === 'next') {
      if (currentIndex === 0) {
        this.elInner.style.transform = `translateX(-${this.len * elWidth}px)`;
        setTimeout(() => {
          const { transition } = this.elInner.style;
          this.elInner.style.transition = 'none';
          this.elInner.style.transform = `translateX(0)`;
          setTimeout(() => {
            this.elInner.style.transition = transition;
          }, 200);
        }, 200);
      } else {
        this.elInner.style.transform = `translateX(-${currentIndex * elWidth}px)`;
      }
    }
    if (dir === 'prev') {
      if (currentIndex === this.len - 1) {
        const { transition } = this.elInner.style;
        this.elInner.style.transition = 'none';
        this.elInner.style.transform = `translateX(-${this.len * elWidth}px`;
        setTimeout(() => {
          this.elInner.style.transition = transition;
          this.elInner.style.transform = `translateX(-${(this.len - 1) * elWidth}px)`;
        }, 200);
      } else {
        this.elInner.style.transform = `translateX(-${currentIndex * elWidth}px)`;
      }
    }

    this.dotItemList.forEach((el, elIdx) => {
      el.className = 'dot-list-item';
      if (elIdx === currentIndex) {
        el.classList.add('active');
      }
    });
  }
}

export default CarouselSlide;

瞬间看起来代码整洁多了。。。

刚才我们把公共逻辑部分的代码提取到了 Carousel 这个父类里面,已经能够提取使用了,但是我们还可以再进一步简化代码 - 直接使用 Carousel 就可以直接生成CarouselFadeCarouselSlide以及其它需要扩展的轮播图模块,并直接调用。

  1. 在 Carousel 上面直接添加一个静态方法 create 用于生成 Carousel 的子类并进行实例化调用
js 复制代码
class Carousel {
  // ...

  static create(type, selector, options) {
    let getter = null;

    switch (type) {
      case 'slide':
        getter = () => import('./CarouselSlide');
        break;
      case 'fade':
      default:
        getter = () => import('./CarouselFade');
        break;
    }
    getter().then(({ default: Constructor }) => {
      new Constructor(selector, options);
    });
  }
}
  1. src/app.js 调用一下 Carousel.create 这个静态方法
js 复制代码
import Carousel from './carousel/Carousel';

Carousel.create('slide', '.J_Carousel', {
  autoplay: true,
  duration: 1500
});

测试结果也是符合预期的,并且 CarouselSlide 这个模块是按需引入的。

项目源码

📎caoursel-demo.zip

相关推荐
ekskef_sef9 分钟前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine64133 分钟前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云1 小时前
npm淘宝镜像
前端·npm·node.js
dz88i81 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr1 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
程序员_三木1 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网2 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工2 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染