前端面试手写代码之-轮播图

2,627 阅读5分钟

轮播图的实现是面试中经常闻到的一个问题,我们今天就来实现一个简单的轮播图,并且看一下AngularReact版本的Ant Design中轮播图是怎么实现的。

简单实现轮播图

网上有很多轮播图的实现方法,主要思路就像图中表示的一样,将所有的图片平铺,通过设置偏移量,将当前图片移动到可视区域。 我们通过代码来实现一下: 首先定义HTML模版

        <div class="container">
            <div class="wrap" style="left:-600px;">
                <img src='./imgs/1.jpg'>
                <img src='./imgs/2.jpg'>
                <img src='./imgs/3.jpg'>
                <img src='./imgs/4.jpg'>
            </div>
            <div class="buttons">
                <span>1</span>
                <span>2</span>
                <span>3</span>
                <span>4</span>
            </div>
            <a href="javascript:;" class="arrow arrow_left">&lt;</a>
            <a href="javascript:;" class="arrow arrow_right">&gt;</a>
        </div>

这里主要包括三部分:轮播图的图片列表,轮播切换的按钮,左右切换按钮; 然后定义一下样式

            .container {
                position: relative;
                width: 600px;
                height: 400px;
                margin:100px auto 0 auto;
                box-shadow: 0 0 5px green;
                overflow: hidden;
            } 
            .wrap {
                position: absolute;
                width: 4200px;
                height: 400px;
                z-index: 1;
            }
            .container .wrap img {
                float: left;
                width: 600px;
                height: 400px;
                line-height: 400px;
                color: #FFF;
                text-align: center;
                background: rgb(54, 77, 121);
            }
            .container .buttons {
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
                bottom:20px;
                height: 10px;
                z-index: 2;
            }

            .container .arrow {
                position: absolute;
                top: 35%;
                color: green;
                padding:0px 14px;
                border-radius: 50%;
                font-size: 50px;
                z-index: 2;
                display: none;
            }

我在这里没有把全部样式代码写上,反正也不重要,大家可以自由发挥。在样式里有几个要注意的点:

  • 图片只在container中,所以需要限定其宽度和高度并且使用overflow:hidden;将其余的图片隐藏起来,并且我们希望wrap相对于container左右移动,所以设置为relative
  • .wrap{width: 2400px}这里是包裹图片的元素,我们例子中有4张图片,每张600px,实际上宽度应该根据内容动态设置,我们这里先写死,文章后面会改进一下;
  • img {width: 600px;height: 400px;}代码中也写的固定值,后面处理。

现在我们的轮播图是这个样子的。

下面看下javascript实现

  var wrap = document.querySelector(".wrap");
  var next = document.querySelector(".arrow_right");
  var prev = document.querySelector(".arrow_left");
  var currIndex = 1;
  var len = wrap.children.length;

  next.onclick = function () {
    next_pic();
  }
  prev.onclick = function () {
    prev_pic();
  }
  function next_pic () {
    newLeft;
    if(currIndex === len){
      newLeft = 0;
      currIndex = 1;
    }else{
      newLeft = parseInt(wrap.style.left)-600;
      currIndex ++
    }
    wrap.style.left = newLeft + "px";
  }
  function prev_pic () {
    var newLeft;
    if(currIndex === 1){
      newLeft = -1800;
      currIndex = 4;
    }else{
      newLeft = parseInt(wrap.style.left)+600;
      currIndex--;
    }
    wrap.style.left = newLeft + "px";
  }

首先要获取模版中的.wrap元素,我们的轮播效果就是通过设置它的偏移量实现的。然后获取左右切换按钮,并添加了切换的事件。

  var newLeft = parseInt(wrap.style.left)-600;
  wrap.style.left = newLeft + "px";

我们设置.wrapposition属性是absolute,所以只需要修改left的值就可以。要注意的是wrap.style.left是字符串,所以要转换成数字。

另外看下偏移方法里面的边界判断逻辑。 首先我们定义了一个currIndex表示当前显示的图片索引,从1开始;len表示图片的个数。当显示的图片为最后一张时,再点击“下一张”,就会将图片定位到第一张,索引修改为第一张的标识。 现在我们是通过点击按钮实现的切换,那么下面添加自动播放,只需要使用setInterval方法循环切换就可以了。

var timer = null;
function autoPlay () {
  timer = setInterval(function () {
    next_pic();
  },1000);
}
autoPlay();

现在的图片切换过程非常生硬,我们来加上一个过度动画,实现平滑的切换。

.wrap {
  transition: 1s;
}

css3的动画实现非常简单,大家也可以尝试其它方法。 到现在实现了轮播图的基本功能。剩下的每个button上加切换功能就留给大家自己尝试一下。

Ant Design of Angular

(如果没有angualr开发经验可以看下思路,不用管代码) HTML模版的内容和我们自己实现的没有太大区别,只不过为了使组件更加灵活添加了一些配置项,允许业务自己扩展。

<div class="slick-initialized slick-slider" [class.slick-vertical]="nzVertical">
  <div
    #slickList
    class="slick-list"
    tabindex="-1"
    (keydown)="onKeyDown($event)"
    (mousedown)="pointerDown($event)"
    (touchstart)="pointerDown($event)"
  >
    <!-- Render carousel items. -->
    <div class="slick-track" #slickTrack>
      <ng-content></ng-content>
    </div>
  </div>
  <!-- Render dots. -->
  <ul class="slick-dots" *ngIf="nzDots">
    <li
      *ngFor="let content of carouselContents; let i = index"
      [class.slick-active]="content.isActive"
      (click)="goTo(i)"
    >
      <ng-template [ngTemplateOutlet]="nzDotRender || renderDotTemplate" [ngTemplateOutletContext]="{ $implicit: i }">
      </ng-template>
    </li>
  </ul>
</div>

<ng-template #renderDotTemplate let-index>
  <button>{{ index + 1 }}</button>
</ng-template>

下面我们只看一下几个重点方法的实现: 在组件初始化之后会执行一个方法,markContentActive(0),将第一个图片标为当前显示,和我们前面的currIndex一个意思。

  private markContentActive(index: number): void {
    this.activeIndex = index;

    if (this.carouselContents) {
      this.carouselContents.forEach((slide, i) => {
        slide.isActive = index === i;
      });
    }

    this.cdr.markForCheck();
  }

来下看切换方法:

  next(): void {
    this.goTo(this.activeIndex + 1);
  }

  pre(): void {
    this.goTo(this.activeIndex - 1);
  }

  goTo(index: number): void {
    if (this.carouselContents && this.carouselContents.length && !this.isTransiting) {
      const length = this.carouselContents.length;
      const from = this.activeIndex;
      const to = (index + length) % length;
      this.isTransiting = true;
      this.nzBeforeChange.emit({ from, to });
      this.strategy.switch(this.activeIndex, index).subscribe(() => {
        this.scheduleNextTransition();
        this.nzAfterChange.emit(index);
        this.isTransiting = false;
      });
      this.markContentActive(to);
      this.cdr.markForCheck();
    }
  }

这里使用goTo方法实现切换,接收参数为要跳转的位置索引。这里面执行了一个this.strategy.switch方法,传入当前索引以及要跳转的索引值。

    this.strategy =
      this.nzEffect === 'scrollx'
        ? new NzCarouselTransformStrategy(this, this.cdr, this.renderer)
        : new NzCarouselOpacityStrategy(this, this.cdr, this.renderer);

this.strategy根据参数判断采用哪种切换动画。默认使用transform.

const rect = carousel.el.getBoundingClientRect();
    this.slickListEl = carousel.slickListEl;
    this.slickTrackEl = carousel.slickTrackEl;
    this.unitWidth = rect.width;
    this.unitHeight = rect.height;
    this.contents = contents ? contents.toArray() : [];
    this.length = this.contents.length;

这里通过获取页面元素算出轮播图当前的widthheight,用来计算偏移量。

  withCarouselContents(contents: QueryList<NzCarouselContentDirective> | null): void {
    super.withCarouselContents(contents);

    const carousel = this.carouselComponent!;
    const activeIndex = carousel.activeIndex;

    if (this.contents.length) {
      if (this.vertical) {
        this.renderer.setStyle(this.slickListEl, 'height', `${this.unitHeight}px`);
        this.renderer.setStyle(this.slickTrackEl, 'height', `${this.length * this.unitHeight}px`);
        this.renderer.setStyle(
          this.slickTrackEl,
          'transform',
          `translate3d(0, ${-activeIndex * this.unitHeight}px, 0)`
        );
      } else {
        this.renderer.setStyle(this.slickTrackEl, 'width', `${this.length * this.unitWidth}px`);
        this.renderer.setStyle(this.slickTrackEl, 'transform', `translate3d(${-activeIndex * this.unitWidth}px, 0, 0)`);
      }

      this.contents.forEach((content: NzCarouselContentDirective) => {
        this.renderer.setStyle(content.el, 'position', 'relative');
        this.renderer.setStyle(content.el, 'width', `${this.unitWidth}px`);
      });
    }
  }

与我们修改left属性不同,这里使用transform属性切换位置。

在这个里面有一个scheduleNextTransition方法,主要用来判断是否需要自动播放,然后通过setTimeout方法间隔一段时间之后再执行goTo,实现了自动轮播功能。

  private scheduleNextTransition(): void {
    this.clearScheduledTransition();
    if (this.nzAutoPlay && this.nzAutoPlaySpeed > 0 && this.platform.isBrowser) {
      this.transitionInProgress = setTimeout(() => {
        this.goTo(this.activeIndex + 1);
      }, this.nzAutoPlaySpeed);
    }
  }

可以看出它的实现思路和我们上面实现的基本相同。

Ant Design for React

React版本的实现方法大同小异,偏移量处理也是通过transform实现的。

  if (spec.useTransform) {
    let WebkitTransform = !spec.vertical
      ? "translate3d(" + spec.left + "px, 0px, 0px)"
      : "translate3d(0px, " + spec.left + "px, 0px)";
    let transform = !spec.vertical
      ? "translate3d(" + spec.left + "px, 0px, 0px)"
      : "translate3d(0px, " + spec.left + "px, 0px)";
    let msTransform = !spec.vertical
      ? "translateX(" + spec.left + "px)"
      : "translateY(" + spec.left + "px)";
    style = {
      ...style,
      WebkitTransform,
      transform,
      msTransform
    };
  } else {
    if (spec.vertical) {
      style["top"] = spec.left;
    } else {
      style["left"] = spec.left;
    }
  }

而对于自动播放则选择了setInterval

  autoPlay = playType => {
    if (this.autoplayTimer) {
      clearInterval(this.autoplayTimer);
    }
    const autoplaying = this.state.autoplaying;
    if (playType === "update") {
      if (
        autoplaying === "hovered" ||
        autoplaying === "focused" ||
        autoplaying === "paused"
      ) {
        return;
      }
    } else if (playType === "leave") {
      if (autoplaying === "paused" || autoplaying === "focused") {
        return;
      }
    } else if (playType === "blur") {
      if (autoplaying === "paused" || autoplaying === "hovered") {
        return;
      }
    }
    this.autoplayTimer = setInterval(this.play, this.props.autoplaySpeed + 50);
    this.setState({ autoplaying: "playing" });
  };