You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

原生JavaScript滑块如何实现窗口resize时宽度自适应

纯JS轮播滑块窗口缩放自适应修复方案

问题原因

组件存在3个核心问题导致无法跟随窗口宽度自适应:

  • 滑块单页宽度slideWidth、滑动停靠位置数组slidePositions、容器宽度仅在首次初始化时计算一次,窗口尺寸变化后没有重新计算
  • 窗口resize时没有同步更新滑块的transform偏移值,会出现位置错位、显示不全、留白溢出
  • 原有代码事件兼容有bug:桌面端使用pointer事件时仍读取e.touches[0],桌面端无touches属性会直接报错

修复方案

  • 将宽度计算、位置初始化逻辑抽为独立方法,支持重复调用
  • 增加窗口resize事件监听,加防抖避免频繁触发,触发时重新计算宽度、位置数组,根据当前所在页码无动画更新滑块位置
  • 兼容桌面/移动端事件坐标取值,修复事件报错问题
  • 移除JS中硬写容器固定宽度的逻辑,依靠CSS弹性布局自动适配

修复后完整代码

JavaScript代码

(function() {
    let curTranslateX = 0;
    let curPageNum = 0;
    let dots = null;
    let slideWidth = 0;
    let duration = 300;
    let pointStart, pointMove, pointEnd;
    let slidePositions = [];
    let slideContainer = document.querySelector('.container_slider');
    let slideWrapper = slideContainer.querySelector('.slide_wrapper');
    let slideItems = [...slideWrapper.querySelectorAll('.slide-item')];
    let resizeTimer = null;
    let negMaxWidth = 0;

    const utils = {
        hasClass: function(elem, className) {
            return(new RegExp('(\\s|^)' + className + '(\\s|$)')).test(elem.className);
        },
        addClass: function(elem, className) {
            if(!arguments.length) return;
            if(typeof className === 'undefined' || this.hasClass(elem, className)) return;
            let newClasses = elem.className.split(' ');
            newClasses.push(className);
            elem.className = newClasses.join(' ');
        },
        removeClass: function(elem, className) {
            if(!arguments.length) return;
            if(typeof className === 'undefined' || !this.hasClass(elem, className)) return;
            let classes = elem.className.split(' ');
            classes = classes.filter(cls => cls !== className);
            elem.className = classes.join(' ');
        },
        isMobile: function() {
            return(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i).test(navigator.userAgent);
        },
        getEventPoint: function(e) {
            return e.touches ? e.touches[0] : e;
        }
    }

    var slide = {
        calcSlideSize: function() {
            slideWidth = slideContainer.offsetWidth;
            slidePositions = [];
            negMaxWidth = -slideWidth * (slideItems.length - 1);
            for(let i = 0, wtd = 0; i < slideItems.length; i++, wtd -= slideWidth) {
                slidePositions.push(wtd);
            }
        },
        setTranslate: function(duration, offsetX) {
            slideWrapper.style.transitionDuration =  `${duration}ms`;
            slideWrapper.style.transform = `translate3d(${offsetX}px, 0px, 0px)`;
            curTranslateX = offsetX;
        },
        setCurrentPage: function(num) {
            if(curPageNum !== -1) {
                utils.removeClass(dots[curPageNum], 'dot-active');
                utils.removeClass(slideItems[curPageNum], 'slide-active');
            }
            num = (typeof num === 'undefined') ? 0 : num;
            curPageNum = num;
            utils.addClass(dots[curPageNum], 'dot-active');
            utils.addClass(slideItems[curPageNum], 'slide-active');
        },
        gotoPage: function(num, useAnimate = true) {
            if(num < 0 || num > dots.length - 1) return;
            let transDuration = useAnimate ? duration : 0;
            slide.setTranslate(transDuration, slidePositions[num]);
            setTimeout(() => {
                slide.setCurrentPage(num);
            }, transDuration / 2);
        },
        bindTouchEvent: function() {
            slide.calcSlideSize();
            let startX,
                startY,
                initialPos = 0,
                moveDist = 0,
                direction = 0,
                isMove = false,
                startT = 0,
                isPointOut = true;

            slideContainer.addEventListener(pointStart, function(e) {
                e.preventDefault();
                let startPoint = utils.getEventPoint(e);
                if(!isPointOut && e.touches && e.touches.length !== 1) return;
                startX = startPoint.pageX;
                startY = startPoint.pageY;
                initialPos = curTranslateX;
                startT = +new Date();
                isMove = false;
                isPointOut = false;
            }, false);

            slideContainer.addEventListener(pointMove, function(e) {
                if(isPointOut) return
                let movePoint = utils.getEventPoint(e);
                let deltaX = movePoint.pageX - startX;
                let offsetX = initialPos + deltaX;
                if(offsetX > 0 || offsetX < negMaxWidth) {
                    offsetX -= (deltaX / 2);
                }
                slide.setTranslate(0, offsetX);
                isMove = true;
                deltaX = offsetX - initialPos;
                moveDist = deltaX;
                direction = deltaX > 0 ? 0 : 1;
            }, false);

            slideContainer.addEventListener(pointEnd, function(e) {
                e.preventDefault();
                let deltaT = +new Date() - startT;
                if(!isMove) {
                    if(utils.hasClass(e.target, 'slide-button-prev')) {
                        if(curPageNum === 0) return;
                        slide.gotoPage(curPageNum - 1);
                    } else if(utils.hasClass(e.target, 'slide-button-next')) {
                        if(curPageNum === dots.length - 1) return;
                        slide.gotoPage(curPageNum + 1);
                    }
                    return;
                }
                if(isPointOut) return;
                isPointOut = true;
                let offsetX;
                if(deltaT < 300 || Math.abs(moveDist) > slideWidth / 2) {
                    offsetX = direction === 0 ? curTranslateX + slideWidth - moveDist : curTranslateX - slideWidth - moveDist;
                    offsetX = offsetX > 0 ? 0 : offsetX;
                    offsetX = offsetX < negMaxWidth ? negMaxWidth : offsetX;
                } else {
                    offsetX = curTranslateX - moveDist;
                }
                slide.setTranslate(duration, offsetX);
                let newPageNum = Math.round(Math.abs(offsetX) / slideWidth);
                setTimeout(() => {
                    slide.setCurrentPage(newPageNum);
                }, duration / 2);
            }, false);

            window.addEventListener('resize', function() {
                clearTimeout(resizeTimer);
                resizeTimer = setTimeout(() => {
                    slide.calcSlideSize();
                    slide.gotoPage(curPageNum, false);
                }, 200);
            })
        },
        init: (function() {
            document.addEventListener('DOMContentLoaded', function() {
                dots = [...document.querySelectorAll('.slide-navbar span')];
                dots.forEach(dot => {
                    utils.addClass(dot, 'dot');
                });
                if(utils.isMobile()) {
                    pointStart = 'touchstart';
                    pointMove = 'touchmove';
                    pointEnd = 'touchend';
                } else {
                    pointStart = 'pointerdown';
                    pointMove = 'pointermove';
                    pointEnd = 'pointerup';
                }
                slide.bindTouchEvent();
                slide.setCurrentPage();
            }, false);
        })()
    };
})();

CSS代码(原有样式无需修改,可直接使用)

.container_box {
    max-width: 1400px;
    margin: 0 auto;
}

.container_slider {
  position: relative;
  overflow: hidden;
}

.slide_wrapper {
  position: relative;
  z-index: 1;
  display: flex;
  transition-property: transform;
}

.container_slider .slide-item {
  z-index: 1;
  display: flex;
  flex-shrink: 0;
  width: 100%;
  height: 300px;
  user-select: none;
  background-size: cover;
  background-position: 50%;
  background-repeat: no-repeat;
}

.slide-navbar {
  position: absolute;
  right: 0;
  left: 0;
  bottom: 0;
  text-align: center;
  font-size: 0;
  z-index: 2;
}

.dot {
  display: inline-block;
  margin: 0 4px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: rgba(255, 255, 255, 0.5);
}

.dot-active {
  width: 20px;
  border-radius: 6px;
  background-color: rgba(233, 233, 233, .9);
}

.slide-button-prev,
.slide-button-next {
  display: inline-block;
  position: absolute;
  top: 50%;
  width: 40px;
  height: 60px;
  z-index: 2;
  color: rgba(233, 233, 233, .9);
  text-align: center;
  font-weight: 500;
}
.slide-button-prev {
  left: 0;
  transform: translateY(-50%);  
}
.slide-button-next {
  right: 0;
  transform: translateY(-50%);
}

HTML代码(原有结构无需修改,可直接使用)

<div class="container_box">
<div class="container_slider">
  <div class="slide_wrapper">
    <div class="slide-item slide-active" style="background-color:green;"></div>
    <div class="slide-item" style="background-color:blue;"></div>
    <div class="slide-item" style="background-color:black;"></div>
  </div>

  <div class="control_slider slide-button-prev"><</div>
  <div class="control_slider slide-button-next">></div>

  <div class="slide-navbar">
    <span class="dot dot-active">1</span>
    <span class="dot">2</span>
    <span class="dot">3</span>
  </div>
</div>
</div>

修复效果

  • 窗口拉伸/缩放时自动重新计算滑块宽度,不会出现固定宽度、内容溢出或留白
  • 缩放过程中保持当前所在滑块页位置,不会跳回第一页或出现错位
  • 修复桌面端滑动事件报错问题,桌面端鼠标拖拽、移动端触摸滑动均可正常使用
  • 防抖处理避免resize过程中频繁计算造成的卡顿

内容的提问来源于stack exchange,提问作者Salis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 09:48:15