利用transform、transition实现轮播图以及解决最后一张图切换时浏览器渲染的坑

基本思路

实现思路就是依据一般无缝轮播图的做法,将第一张图片复制插入轮播图队尾,以起到平滑过渡最后一张图至第一张图的作用,过程如下图所示,由1号向前滑动过渡至4号原理亦如此。

image.png

transform

选择transform的原因,在于它的性能优势。transform的属性计算在浏览器渲染管线的最后阶段即Composite阶段,如图所示,这意味着transform的属性计算不会影响页面的回流和重绘。它的原理是在Composite阶段通过原先元素的位图信息缓存进行线性映射来完成属性计算,在动画过程中不会改变原先的位图信息缓存,从而也就避免触发回流和重绘。

image.png

实现过程以及渲染的问题

主要代码

/**
* params: new_index 目标索引值
* return: 返回轮播图当前图片位置
*/

function update(new_index) {

        if (new_index < 0) {
            new_index = data.length - 1;
            _move(-width * data.length, false);
            _index = 0;
            dataBox.offsetWidth;     //强行刷新渲染队列
        }

        if (new_index > data.length) {
            new_index = 1;
            _move(0, false);
            _index = 0;
            dataBox.offsetWidth;
        }
        
        _move(-new_index * width, true);
        _index = new_index;

        return (_index % data.length);
    }
    
function _move(distance, animation) {
        dataBox.style.transition = animation ? 'transform .5s' : 'none';
        dataBox.style.transform = `translateX(${distance}px)`;
    }
复制代码

offsetWidth刷新渲染队列

之所以在由队尾的1号图片无动画切换至队首1号图片后,使用offsetWidth强制刷新渲染队列,是因为现在的浏览器会把一些dom的属性变更操作,加入到渲染队列中,最后统一刷新渲染队列进行一次回流,从而减少页面的回流次数。但是,这会使得像上面代码所示的transition transform属性的两次更改在回流的过程中只保留了最后一次的值。不刷新和刷新效果分别如下图所示。刷新渲染队列不仅限于offsetWidth,例如offsetTop,offsetLeft,offsetWidth,offsetHeight,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight等都可以触发。我也有看到利用requestAnimationFrame(()=>{requestAnimationFrame(callback)})这样的方式去让callback内的样式变更,放在下一帧去回流。

2021-05-22-21-10-05.gif

2021-05-22-21-12-29.gif

完整代码

function Slide(dom, data) {

    let app, width, height, dataBox, _index, timer, timeout;

    function _create() {

        if (dom == null || data == null || data.length < 2) {
            throw new Error('create失败');
        }

        app = document.getElementById(dom);
        app.style.cssText = 'overflow: hidden';
        appStyle = getComputedStyle(app);
        width = parseInt(appStyle.width);
        height = parseInt(appStyle.height);
        dataBox = document.createElement('div');
        _index = 0;

        dataBox.style.cssText = `
            position: relative;
            height: ${height}px;
            width: ${(data.length + 1) * width}px;
        `;

        imgCSS = `
            width: ${width}px;
            height: ${height}px;
            float: left;
        `

        data.forEach(img => {
            let el = document.createElement('img');
            el.src = img;
            el.style.cssText = imgCSS;
            dataBox.appendChild(el);
        });

        let el = document.createElement('img');
        el.src = data[0];
        el.style.cssText = imgCSS;
        dataBox.appendChild(el);

        app.appendChild(dataBox);

    }

    function _move(distance, animation) {
        dataBox.style.transition = animation ? 'transform .5s' : 'none';
        dataBox.style.transform = `translateX(${distance}px)`;
    }

    function _autoPlay() {
        clearTimeout(timer);
        timer = setTimeout(() => {
            next();
            _autoPlay();
        }, timeout);
    }

    function update(new_index) {

        if (new_index < 0) {
            new_index = data.length - 1;
            _move(-width * data.length, false);
            _index = 0;
            dataBox.offsetWidth;
        }

        if (new_index > data.length) {
            new_index = 1;
            _move(0, false);
            _index = 0;
            dataBox.offsetWidth;
        }
        
        _move(-new_index * width, true);
        _index = new_index;

        return (_index % data.length);
    }

    function next() {
        return update(_index + 1);
    }

    function pre() {
        return update(_index - 1);
    }

    function setTime(ms = 1000) {
        let start = () => { _autoPlay() }
        let end = () => { clearTimeout(timer) }

        if (ms == -1) {
            app.removeEventListener('mouseover', end);
            app.removeEventListener('mouseleave', start);
            end();
        } else {
            timeout = ms < 500 ? 1000 : ms;
            app.addEventListener('mouseover', end);
            app.addEventListener('mouseleave', start);
            start();
        }
    }

    _create();

    return { next, pre, update, setTime }; //setTime输入-1表示关闭动画
}

复制代码

参考链接

从原理上理解,如何利用 CSS3 transition 打造无限轮播图

高性能Web动画和渲染原理系列(2)——渲染管线和CPU渲染

高性能Web动画和渲染原理系列(3)——transform和opacity为什么高性能

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享