前端面试100道手写题(7)—— 循环轮播图

简介: 前端面试100道手写题(7)—— 循环轮播图

前言

循环轮播图,基本上大家用的都是现有组件,如果要让你自己设计实现一个,其实最主要的两个点:循环算法和滚动动画

手写难度:⭐️⭐️

涉及知识点:

  • 循环播放的思路
  • CSS 动画,transtion和 transform
  • Web Component 自定义组件

轮播图

大家最常用的轮播图基本上就是 swiper.js,不仅适配 PC 端和移动端,同时包含多种实际应用场景。但是目前我们只需要实现其中一种场景即可——循环轮播图,大概示例图如下:

<swiper-container speed="500" loop="true">
  <swiper-slide>Slide 1</swiper-slide>
  <swiper-slide>Slide 2</swiper-slide>
  <swiper-slide>Slide 3</swiper-slide>
  ...
</swiper-container>

Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6

大概效果如下:

image.png

实现思路

在研究实现思路前,我们先确定一下要实现的目标,如下:

  1. 采用Web Component去实现两个自定义标签<swiper-container><swiper-slide>
  2. <swiper-container>标签支持属性配置,如:speedloop

实现思路如下:

  • <swiper-container>容器为 flex 容器,里面包含一个wrapper容器用于装载所有的<swiper-slide>
  • <swiper-slide> 采用横向布局,当切换下一个的时候,使用transform:translate(x,y)wrapper向左移动进行展示下一个slide
  • loop为 true的时候,支持循环播放
  • 循环播放逻辑为,在最后一个<swiper-slide>后面复制第一个<swiper-slide>
  • 当最后一个继续点击next的时候,会把复制第一个展示
  • 当第一个(复制)展示后,点击下一步的时候,取消动画效果,将wrapper位置移动到第一个
  • 然后利用setTimeout(0)延时执行,增加动画动画效果,将wrapper位置移动到第二个

为了更好理解循环动画思路,为了更好的展示效果,我将container取消了overflow:hidden,具体动画如下:

image.png

整个轮播图的 DOM 结构如下:

image.png

代码实践

我们将通过Web Component规范去定义上述两个组件,分别是<swiper-container><swiper-slide>

Swiper-Container组件

Swiper-Container 负责实现容器和控制轮播图滚动事件,等于是整个轮播图的核心,具体代码划分如下:

<template id="swiper-container">
<style>
  /**为节省文字,忽略样式,可以到 Github去看看开源完整示例代码 */
</style>
<div class="swiper-container">
    <div class="swiper-container-wrapper">
        <slot></slot>
    </div>
    <div class="swiper-pagination">
    </div>
    <div class="swiper-button-next"></div>
    <div class="swiper-button-prev"></div>
</div>
</template>
<script>
class SwiperContainer extends HTMLElement {
    constructor() {
        super();
        const template = document.getElementById('swiper-container');
        const templateContent = template.content;
        const shadowRoot = this.attachShadow({ mode: "open" });
        shadowRoot.appendChild(templateContent.cloneNode(true));
        this.loop = this.getAttribute('loop') === 'true';
        this.speed = this.getAttribute('speed') || 500;
        this.currentIndex = 0;
    }
    /**
     * 当 custom element首次被插入文档DOM时,被调用。
     */
    connectedCallback() {
        // 由于 slot 的内容是异步的,所以需要等待 slot 的内容渲染完成后再初始化
        setTimeout(() => {
            this.wrapper = this.shadowRoot.querySelector('.swiper-container-wrapper');
            this.slides = this.querySelectorAll('swiper-slide');
            this.pagination = this.shadowRoot.querySelector('.swiper-pagination');
            this.next = this.shadowRoot.querySelector('.swiper-button-next');
            this.prev = this.shadowRoot.querySelector('.swiper-button-prev');
            this.prev.classList.add('swiper-button-prev-disabled');
            this.slideWidth = this.slides[0].offsetWidth;
            this.slideCount = this.slides.length;
            this.slides.forEach((slide) => {
                slide.style.height = '100%';
            });
            this.init();
        }, 0);
    }
    /**
     * 初始化操作
     */
    init() {
        this.wrapper.style.width = this.slideWidth * this.slideCount + 'px';
        this.wrapper.style.transform = `translate3d(-${this.slideWidth * this.currentIndex}px, 0, 0)`;
        this.wrapper.style.transition = `transform ${this.speed}ms ease-in-out`;
        // 判断是否可以循环
        let slideCount = this.slideCount;
        if (this.slideCount > 1 && this.loop) {
            this.cloneFirstSlide();
            slideCount = this.slideCount - 1;
        }
        this.pagination.innerHTML = '';
        const bulletFragment = document.createDocumentFragment();
        for (let i = 0; i < slideCount; i++) {
            const bullet = document.createElement('div');
            bullet.classList.add('swiper-pagination-bullet');
            bullet.dataset.index = i;
            bulletFragment.appendChild(bullet);
        }
        bulletFragment.children[0].classList.add('swiper-pagination-bullet-active');
        this.pagination.appendChild(bulletFragment);
    }
    /**
     * 绑定相关事件
     */
    bindEvents(){
      this.next.addEventListener('click', () => {
            this.nextSlide();
        });
        this.prev.addEventListener('click', () => {
            this.prevSlide();
        });
        this.pagination.addEventListener('click', (e) => {
            const index = e.target.dataset.index;
            if (index) {
                this.goToSlide(index);
            }
        });
    }
    /**
     * 将第一个 slider 复制到最后
     */
    cloneFirstSlide() {
        const firstSlide = this.slides[0].cloneNode(true);
        this.wrapper.appendChild(firstSlide);
        this.slideCount++;
    }
    /**
     * 跳转到下一个的 slider
     */
    nextSlide() {
        // 如果不是循环的,且已经是最后一个,就不执行
        if (!this.loop && this.currentIndex >= this.slideCount - 1) {
            return;
        }
        this.currentIndex++;
        // 改变下一个 icon 的状态
        if (!this.loop && this.currentIndex >= this.slideCount - 1) {
            this.next.classList.add('swiper-button-next-disabled');
        } else {
            this.next.classList.remove('swiper-button-next-disabled');
            this.prev.classList.remove('swiper-button-prev-disabled');
        }
        console.log(this.currentIndex, this.slideCount)
        // 判断是不是最后一个 如果最后一个,等动画执行完毕,瞬间跳到第一个
        if (this.currentIndex >= this.slideCount) {
            this.currentIndex = 0;
            this.wrapper.style.transition = 'none';
            this.wrapper.style.transform = `translate3d(-${this.slideWidth * this.currentIndex}px, 0, 0)`;
            setTimeout(() => {
                this.wrapper.style.transition = `transform ${this.speed}ms ease-in-out`;
                this.currentIndex++;
                this.goToSlide(this.currentIndex);
            }, 0);
        } else {
            this.goToSlide(this.currentIndex);
        }
    }
    /**
     * 跳转到上一个的 slider 
     */
    prevSlide() {
        if (!this.loop && this.currentIndex <= 0) {
            return;
        }
        this.currentIndex--;
        if (!this.loop && this.currentIndex <= 0) {
            this.prev.classList.add('swiper-button-prev-disabled');
        } else {
            this.next.classList.remove('swiper-button-next-disabled');
            this.prev.classList.remove('swiper-button-prev-disabled');
        }
        if (this.currentIndex < 0) {
            this.currentIndex = this.slideCount - 1;
        }
        this.goToSlide(this.currentIndex);
    }
    /**
     * 跳转到指定的 slider
     * @param {*} index 
     */
    goToSlide(index) {
        this.currentIndex = index;
        this.wrapper.style.transform = `translate3d(-${this.slideWidth * this.currentIndex}px, 0, 0)`;
        this.setActivePagination();
    }
    /**
     *  设置当前的 pagination
     */
    setActivePagination() {
        const paginationBullets = this.shadowRoot.querySelectorAll('.swiper-pagination-bullet');
        paginationBullets.forEach((bullet, index) => {
            bullet.classList.remove('swiper-pagination-bullet-active');
        });
        if (this.currentIndex === this.slideCount - 1 && this.loop) {
            paginationBullets[0].classList.add('swiper-pagination-bullet-active');
            return;
        }
        paginationBullets.forEach((bullet, index) => {
            if (index === this.currentIndex) {
                bullet.classList.add('swiper-pagination-bullet-active');
            }
        });
    }
}
// 注册swiper-container组件
customElements.define('swiper-container', SwiperContainer);
</script>

要是不想看代码,可以看这里的方法简要说明:

  • init初始化函数,用了 setTimeout去解决 slot的异步渲染问题,获取一些 dom 节点
  • 其中需要判断是否循环loop,如果需要则需要复制第一个节点到最后cloneFirstSlide
  • bindEvents 绑定 prev、next、pagination等 dom 的 click事件
  • nextSlideprevSlide指的是跳转到下一个节点和上一个节点所需要执行的函数,
  • 其中 nextSlide 函数需要在最后一个节点判断当前是否为 loop,如果 loop为 true,则需要停止动画,同时将 wrapper 容器的 transform 迁移到第一个节点
  • goToSlide 用执行当前需要展示哪个 slide
  • setActivePagination用执行判断 哪个 pagination需要展示高亮样式

Swiper-Slide组件

swiper-slide组件实现起来就很简单,只需要满足样式展示即可,不过有一点需要注意,就是由于swiper-container是flex布局,所以需要设置swiper-slide的样式不允许缩放flex-shrink: 0;,完整代码如下:

/**
 * 轮播组件,子元素组件
 */
class SwiperSlide extends HTMLElement {
    constructor() {
        super();
        const template = document.createElement('template');
        template.innerHTML = `
        <style>
        .swiper-slide {
            height: 100%;
            display: flex;
            justify-content: center;
            align-items: center;
            /* 防止缩小 */
            flex-shrink: 0;
            border: 1px solid #000;
            background-color: #478703;
            color: #fff;
            font-size: 24px;
            text-align: center;
        }
        </style>
        <div class="swiper-slide"><slot></slot></div>
        `;
        const templateContent = template.content;
        const shadowRoot = this.attachShadow({ mode: "open" });
        shadowRoot.appendChild(templateContent.cloneNode(true));
    }
}
// 注册swiper-slide组件
customElements.define('swiper-slide', SwiperSlide);

本文所有代码都已放到100道前端精品面试题,中的前端面试100道手写题(7)—— 循环轮播图,如果有帮助到你,可以帮忙给个star 即可。

参考资料

目录
相关文章
|
1月前
|
前端开发 JavaScript 网络协议
前端最常见的JS面试题大全
【4月更文挑战第3天】前端最常见的JS面试题大全
54 5
|
4月前
|
设计模式 前端开发 算法
No210.精选前端面试题,享受每天的挑战和学习
No210.精选前端面试题,享受每天的挑战和学习
No210.精选前端面试题,享受每天的挑战和学习
|
4月前
|
消息中间件 缓存 前端开发
No209.精选前端面试题,享受每天的挑战和学习
No209.精选前端面试题,享受每天的挑战和学习
No209.精选前端面试题,享受每天的挑战和学习
|
4月前
|
前端开发 JavaScript Java
No208.精选前端面试题,享受每天的挑战和学习
No208.精选前端面试题,享受每天的挑战和学习
No208.精选前端面试题,享受每天的挑战和学习
|
4月前
|
存储 缓存 前端开发
No198.精选前端面试题,享受每天的挑战和学习
No198.精选前端面试题,享受每天的挑战和学习
|
3月前
|
存储 缓存 Java
明知面试要问spring循环依赖,很多人还是搞不懂!
Spring中的循环依赖一直是Spring中一个很重要的话题,一方面是因为源码中为了解决循环依赖做了很多处理,另外一方面是因为面试的时候,如果问到Spring中比较高阶的问题,那么循环依赖必定逃不掉。如果你回答得好,那么这就是你的必杀技,反正,那就是面试官的必杀技,这也是取这个标题的原因,当然,本文的目的是为了让你在之后的所有面试中能多一个必杀技,专门用来绝杀面试官!
33 0
|
4月前
|
存储 JSON 前端开发
No206.精选前端面试题,享受每天的挑战和学习
No206.精选前端面试题,享受每天的挑战和学习
No206.精选前端面试题,享受每天的挑战和学习
|
2月前
|
存储 缓存 监控
2024年春招小红书前端实习面试题分享
春招已经拉开帷幕啦! 春招的拉开,意味着新一轮的求职大战已经打响,希望每位求职者都能充分准备,以最佳的状态迎接挑战,找到心仪的工作,开启职业生涯的新篇章。祝愿每位求职者都能收获满满,前程似锦!
80 3
|
2月前
|
前端开发 数据可视化 安全
2024金三银四必看前端面试题!简答版精品!
2024金三银四必看前端面试题!2w字精品!简答版 金三银四黄金期来了 想要跳槽的小伙伴快来看啊
98 3
|
3月前
|
存储 前端开发 JavaScript
前端面试:如何实现并发请求数量控制?
前端面试:如何实现并发请求数量控制?
100 0