js原生操作:轮播图、下拉刷新、上拉无限加载记录

该文章已生成可运行项目,

js代码一定要写在body后面!不然会报错,网页要先加载

轮播图,纯js实现

静态组件:

1.用div存需要轮播的图片

2.点击切换的按钮prev、next

3.下面的小圆点,用一个div存三个点,第一个点添加active类默认激活

样式:

外容器:由于图片是排一起的,所以外部div要防溢出;动画过渡transition transform平滑过渡

前后切换按钮:开启绝对定位,在外容器的50%处(注意要transform: translateY(-50%););cursor:pointer;鼠标变成小手

下面三个小点:绝对定位,定在下方;left50%(注意这里也要transform: translateX(-50%);)

注意:这里的按钮和小圆点都需要高于图片图层,不然无法点击

transision

js逻辑:

在 DOM 加载完成后执行的轮播图逻辑平滑过渡动画 + 自动轮播 + 无缝循环 + 手动控制 + 圆点控制 

大体结构如下:

  1. 获取元素(slides、按钮、圆点)

  2. 初始化变量(索引、计时器、动画锁)

  3. 显示指定幻灯片 showSlide(i)

  4. 动画结束处理(transitionend)

  5. 自动轮播(setInterval)

  6. 左右按钮事件

  7. 底部圆点事件

  8. 页面切后台时同步状态

无缝循环:最后一张为第一张图,实现无缝切换

动画锁:防止用户快速点击导致索引错乱

// 轮播图操作
document.addEventListener('DOMContentLoaded', () => {
    // ----------- 获取元素 -----------
    const inner = document.querySelector('.inner-carousel');
    const slides = document.querySelectorAll('.slide');
    const prevBtn = document.querySelector('.prev');
    const nextBtn = document.querySelector('.next');
    const dots = document.querySelectorAll('.dot');

    // ----------- 初始化变量 -----------
    const total = slides.length - 1; // 最后一张是克隆图
    let index = 0;                   // 当前显示的 slide 索引
    let timer;                        // 自动轮播定时器
    let isAnimating = false;          // 动画锁,防止快速点击导致 index 错乱

    // ----------- 显示指定 slide -----------
    function showSlide(i) {
        if (isAnimating) return;      // 动画未完成时禁止执行
        isAnimating = true;

        index = i; //更新index

        // 添加平滑过渡动画
        inner.style.transition = 'transform 0.6s ease';
        inner.style.transform = `translateX(-${index * 100}%)`;

        // 更新底部圆点高亮
        dots.forEach(dot => dot.classList.remove('active'));
        //index % total 用于 处理最后一张克隆图 时,让圆点指示第一张
        dots[index % total].classList.add('active');
    }

    // ----------- 监听动画结束事件,用于无缝循环 ----------- 
    inner.addEventListener('transitionend', () => {
        isAnimating = false; // 动画结束解锁

        // 如果到克隆图(最后一张),瞬间跳回第一张,无缝循环
        if (index === total) {
            inner.style.transition = 'none';
            index = 0;
            inner.style.transform = 'translateX(0%)';
        }
    });

    // ----------- 自动轮播 ----------- 
    timer = setInterval(() => showSlide(index + 1), 4000);

    // ----------- 左右按钮事件 ----------- 
    nextBtn.addEventListener('click', () => {
        clearInterval(timer);          // 点击按钮时先暂停自动轮播
        showSlide(index + 1);
        //重启自动轮播
        timer = setInterval(() => showSlide(index + 1), 4000);
    });

    prevBtn.addEventListener('click', () => {
        clearInterval(timer);

        if (isAnimating) return; // 动画未结束,先阻止

        // 如果在第一张,直接跳到克隆图(最后一张),再执行动画向前切换
        if (index === 0) {
            inner.style.transition = 'none';
            index = total;
            inner.style.transform = `translateX(-${index * 100}%)`;

            // 用 setTimeout 确保下一帧再执行动画
            setTimeout(() =>{ 
                isAnimating = false; // 手动解锁
                showSlide(index - 1), 20});
        } else {
            showSlide(index - 1);
        }

        //重启动画轮播
        timer = setInterval(() => showSlide(index + 1), 4000);
    });

    // ----------- 底部圆点悬停事件 ----------- 
    dots.forEach((dot, i) => {
        dot.addEventListener('mouseover', () => { // mouseover 比 mouseenter 更稳
            clearInterval(timer); //暂停自动轮播
            showSlide(i); //切换对应幻灯片
            timer = setInterval(() => showSlide(index + 1), 4000);
        });
    });

    // ----------- 页面切后台再回来时同步状态 ----------- 
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) {
            // 确保 transform 和动画锁状态正确
            inner.style.transition = 'none';
            inner.style.transform = `translateX(-${index * 100}%)`;
            isAnimating = false; // 解除动画锁,不加这句等再切回来就会出bug
            clearInterval(timer);
            timer = setInterval(() => showSlide(index + 1), 4000);
        } else {
            // 页面切走时清理定时器
            clearInterval(timer);
        }
    });

});

下拉刷新

明确什么是上拉下拉:就是手指在往上还是往下,手指往上拉,页面往下滚

主要是移动端会有这个功能。用事件监听,几个触摸时机:

touchstart:触摸周期开始,手指第一次触碰屏幕

touchmove:手指在屏幕上不断移动

touchend:手指离开屏幕,触摸周期结束

注意这里是一个“触摸周期”,一个用户在看网页时会有多次触摸周期

核心逻辑:

获取dom元素和变量准备:获取写好的刷新区域元素(需要设置height=0、overflow hidden,先隐藏起来)、商品部分元素

const refreshArea = document.querySelector('.refresh-area');
const content = document.querySelector('.list');

// 记录触摸起点和当前状态
let startY = 0;           // 触摸起始位置
let distance = 0;         // 下拉距离
let isPulling = false;    // 是否在下拉中
let refreshing = false;   // 是否正在刷新中

ispulling和refreshing的作用都是防止重复触发操作,类似防抖

touchstart时,判断当前滚动条window.scrollY是否在顶部(===0),如果不在顶部说明用户在浏览商品,则不做刷新操作,return(注意这里需要配合refreshing,如果正在刷新中也要直接return)

触摸界面那一刻,我们就当作用户在进行下拉操作,所以ispulling=true

// 监听触摸开始,触发时机:手指第一次接触屏幕时
window.addEventListener('touchstart', e => {
  // 如果滚动条不在顶部,不触发刷新
  // refreshing的作用是阻止刷新过程中再次刷新
  if (window.scrollY !== 0 || refreshing) return;

  startY = e.touches[0].clientY;  // 记录起点
  isPulling = true;
});

触摸事件可能有多个手指同时触屏,所以系统会把所有手指的位置存在一个数组里:e.touches

// 这是一个TouchList,里面每个手指都是一个 Touch 对象。

// 例如:e.touches[0] ,第一个手指

// e.touches[1] ,第二个手指(如果多指触控)

// 而 clientY 表示:当前手指相对于 视口顶部 的垂直坐标(单位是 px)

touchmove时,如果ispulling=false,说明用户并没有在下拉操作,此时不必执行,直接return;

记录下当前的clientY,计算distance = currentY - startY,如果distance小于0,说明用户在往上滑,return;

当distance到一定数值时改变刷新提示文字

// 监听触摸移动,触发时机:手指在屏幕上移动的整个过程,会持续触发

window.addEventListener('touchmove', e => {
  if (!isPulling) return;

  const currentY = e.touches[0].clientY;
  distance = currentY - startY;

  // 如果是向上滑(负值),直接忽略
  if (distance < 0) return;

  // 阻止浏览器默认滚动,让页面停下
  e.preventDefault();

  // 限制最大下拉距离,比如 100px
  const pullDistance = Math.min(distance, 100);

  // 根据距离调整刷新区域高度
  refreshArea.style.height = pullDistance + 'px';

  // 改变提示文字
  if (pullDistance > 60) {
    refreshArea.textContent = '释放刷新 ↑';
  } else {
    refreshArea.textContent = '↓ 下拉刷新';
  }
});

touchend时,就是松开手指了,此时如果distance的距离超过设置的阈值,触发刷新函数triggerRefresh(),没有则隐藏,

重置distance

window.addEventListener('touchend', () => {
  if (!isPulling) return;
  isPulling = false;

  // 如果下拉超过阈值(比如 60px),触发刷新
  if (distance > 60) {
    triggerRefresh();
  } else {
    // 没超过,回弹隐藏
    refreshArea.style.height = '0px';
  }

  distance = 0;
});

刷新函数

刷新时执行,refreshing改为true,刷新区域的文字改成“正在刷新”,这里使用固定数据,setTimeout,来模拟刷新商品,可以改成请求接口

function triggerRefresh() {
  refreshing = true;
  refreshArea.textContent = '正在刷新...';
  refreshArea.style.height = '50px'; // 固定显示高度

  // 模拟异步加载(这里用 setTimeout 模拟)
  setTimeout(() => {
    // 模拟“刷新”商品
    content.innerHTML = '';
    for (let i = 1; i <= 5; i++) {
      const item = document.createElement('div');
      item.className = 'item';
      item.textContent = '新商品 ' + i;
      // 设置样式
      item.style.padding = '25px';
      item.style.fontSize = '20px';
      item.style.minHeight = '80px';
      content.appendChild(item);
    }

    // 显示完成提示
    refreshArea.textContent = '刷新完成';
    // window.loadedCount = content.children.length; // 已刷新商品数量
    window.loadedCount = 0;  // 从数组开头重新加载
    window.loadingLock = false;                   // 解锁无限加载


    // 稍等一下再收起
    setTimeout(() => {
      refreshArea.style.height = '0px';
      refreshing = false;
    }, 500);
  }, 1000); // 模拟1秒网络延迟

  
}

window.loadedCount 和 window.loadingLock 是为了配合无限加载功能,如果是项目可以用状态管理方案

上拉无限加载


核心:对比scrollBottom和documentHeight

// 获取当前滚动位置 + 可视窗口高度(通常不变)
  const scrollBottom = window.scrollY + window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight; // 整个文档高度,包括当前视口外的内容

  // 如果接近底部,并且没有在加载
  if (!window.loadingLock && scrollBottom >= documentHeight - 10) {
    loadMoreItems();
    }

scrollBottom=视口高度(通常是固定的,不会随滚动而变)+ 滚动条位置

documentHeight=展示内容的总高度,包括视口以外的区域

scrollBottom >= documentHeight - 10 代表快划到内容底部,需要触发加载函数了

也需要用状态锁

这里也是模拟商品,没有接口

const content = document.querySelector('.list');
const loading = document.querySelector('.loading');

// 假商品数组,每次加载 3 个
const allItems = ['商品6', '商品7', '商品8', '商品9', '商品10', '商品11', '商品12'];

window.loadingLock = false; //是否在加载
window.loadedCount = 0; //已经加载的内容数量


// 监听滚动事件
window.addEventListener('scroll', () => {
    console.log('scrollY:', window.scrollY, 'innerHeight:', window.innerHeight);
  // 获取当前滚动位置 + 可视窗口高度(通常不变)
  const scrollBottom = window.scrollY + window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight; // 整个文档高度,包括当前视口外的内容

  // 如果接近底部,并且没有在加载
  if (!window.loadingLock && scrollBottom >= documentHeight - 10) {
    loadMoreItems();
    }

});

// 加载更多商品函数
function loadMoreItems() {
  if (window.loadedCount >= allItems.length) return; // 没有更多了

  window.loadingLock = true;            // 加锁
  loading.style.display = 'block'; // 显示 loading 提示

  // 模拟网络延迟
  setTimeout(() => {
    const batch = allItems.slice(loadedCount, loadedCount + 3); // 每次加载 3 个
    // name是数组中的当前元素
    batch.forEach(name => {
        //创建新的div元素
      const div = document.createElement('div');
      div.className = 'item'; //添加类名
      div.textContent = name; 

      // 设置样式
    div.style.padding = '25px';
    div.style.fontSize = '20px';
    div.style.minHeight = '80px';
      content.appendChild(div);
    });

    loadedCount += batch.length; // 更新已加载数量
    loading.style.display = 'none'; // 隐藏 loading
    loadingLock = false;            // 解锁
  }, 800); // 模拟 0.8 秒加载
}

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值