前端面试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 即可。

参考资料

目录
相关文章
|
3月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
17天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
51 1
|
2月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
2月前
|
前端开发 JavaScript
前端基础(八)_JavaScript循环(for循环、for-in循环、for-of循环、while、do-while 循环、break 与 continue)
本文介绍了JavaScript中的循环语句,包括for循环、for-in循环、for-of循环、while循环、do-while循环以及break和continue的使用。
51 1
前端基础(八)_JavaScript循环(for循环、for-in循环、for-of循环、while、do-while 循环、break 与 continue)
|
1月前
|
C语言
经典面试题:嵌入式系统中经常要用到无限循环,怎么样用C编写死循环呢
在嵌入式系统开发中,无限循环常用于持续运行特定任务或监听事件。使用C语言实现死循环很简单,可以通过`while(1)`或`for(;;)`的结构来编写。例如:`while (1) { /* 循环体代码 */ }`,这种写法明确简洁,适用于需要持续执行的任务或等待中断的场景。
|
1月前
|
前端开发 小程序 JavaScript
信前端里的循环显示如何编写代码?
信前端里的循环显示如何编写代码?
66 5
|
3月前
|
存储 XML 移动开发
前端大厂面试真题
前端大厂面试真题
|
28天前
|
Web App开发 JavaScript 前端开发
前端Node.js面试题
前端Node.js面试题
|
2月前
|
JavaScript 前端开发
网页前端课程设计-【模仿】香港中文大学官网,轮播图及div+css布局,js的dom操作
这篇文章介绍了如何模仿香港中文大学官网进行网页前端课程设计,包括使用div+css布局、js的DOM操作以及实现轮播图等技术细节。
|
3月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
【8月更文挑战第18天】
51 2