V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
leaveeel
V2EX  ›  前端开发

如何更好的用原生 js 实现 jquery 的 animate

  •  
  •   leaveeel · 2019-04-17 16:16:52 +08:00 · 2961 次点击
    这是一个创建于 2074 天前的主题,其中的信息可能已经有所发展或是发生改变。

    先说我现在的方法吧

    背景:项目是 react 环境,里面有个滚动数据的功能,没有引入 jquery,个人感觉单独为了一个animate多引一个 jquery 不划算。

    需求:展示获取到的 N 条数据,首屏展示 x 条,每过 t 秒滚动到下一屏。

    实现:主要通过两个定时器,animate控制滚动间隔。Interval控制从滚动开始到滚动结束时长,代码如下。

    目标:实现类似 jqueryanimate的平滑效果,目前在内层的定时器设置 0.001 秒移动步长±1 像素,在小区域内还行,但是半屏或者全屏的滚动就显得速度慢了,修改增加步长又不平滑,有很明显的段落感。而 jqueryanimate中就很流畅,所有动画都在 time 内完成。所以想请教下有没有更好的办法实现,或者有什么轻便的 animate 包。

    代码:

        ……
        
        //底部回滚到顶部
        scroll = () => {
            let dom = document.getElementById('scroll'),
                //获取精确值
                full = +window.getComputedStyle(dom).height.slice(0, -2),
                view = +window.getComputedStyle(dom.parentNode).height.slice(0, -2),
                //储存预设 top
                top = 0,
                //滚动值
                scrollTop = 0,
                //是否到底部
                toTop = false;
            function animate(){
                top = toTop ? 0 : top - view
                let Interval = setInterval(function(){
                    scrollTop = toTop ? scrollTop + 10 : scrollTop - 1
                    dom.style.marginTop = scrollTop + 'px'
                    if(toTop && scrollTop >= top){
                        toTop = false
                        dom.style.marginTop = top + 'px'
                        clearInterval(Interval)
                    }
                    if(!toTop && scrollTop <= top){
                        if(scrollTop - view <= -full) {
                            toTop = true
                        }
                        dom.style.marginTop = top + 'px'
                        clearInterval(Interval)
                    }
                }, 1);
            }
            setInterval(animate, 4000)
        }
        //首尾循环
        //    scroll = () => {
        //        let dom = document.getElementById('scroll'),
        //            //获取精确值
        //            full = +window.getComputedStyle(dom).height.slice(0, -2),
        //            view = +window.getComputedStyle(dom.parentNode).height.slice(0, -2),
        //            li = +window.getComputedStyle(dom.firstChild).height.slice(0, -2),
        //            //滚动值
        //            scrollTop = 0,
        //            //储存 node
        //            liArr = []
        //        function animate(){
        //            //清空
        //            liArr = []
        //            //获取一屏的 node
        //            for (let i = 0; i < Math.ceil(view / li); i++){
        //                liArr.push(dom.childNodes[i])
        //            }
        //            //滚动事件
        //            let Interval = setInterval(function(){
        //                scrollTop -= 1
        //                dom.style.marginTop = scrollTop + 'px'
        //                //滚动到第二屏
        //                if(scrollTop <= -view){
        //                    clearInterval(Interval)
        //                    //重置
        //                    scrollTop = 0
        //                    //移动 node
        //                    liArr.map(item => {
        //                        dom.removeChild(item)
        //                        dom.appendChild(item)
        //                    })
        //                    //复位
        //                    dom.style.marginTop = '0px'
        //                }
        //            }, 1);
        //        }
        //        setInterval(animate, 4000)
        //    }
        
        ……
    

    题外:顺便也可以指导下以上有哪些格式或者代码可以优化的

    第 1 条附言  ·  2019-04-19 10:29:57 +08:00

    改用rAF实现动画,视觉效果可以了。还有点bug,切换标签或者最小化浏览器后rAF还在走,通过visibilityState判断是否执行/取消动画好像也没效果。虽然是用作展示的大屏页面不存在切换标签/最小化的情况,但还是想优化一下。因为有字数限制,只能贴部分主要代码。有什么好方法解决可以提供一下思路,也可以指导一下代码

        ……
    
            let step = timestamp => {
                    //储存起始时间
                    if (!start) start = timestamp
                    //每帧的滚动位置
                    let progress = (timestamp - start) / 2;
                    dom.style.transform = 'translateY(' + -progress + 'px)'
                    //判断是否在前台显示,执行或暂停rAF并清空start
                    if(this.state.tabVisible){
                        stepID = window.requestAnimationFrame(step)
                    }else {
                        window.cancelAnimationFrame(stepID)
                        start = null
                    }
                    if (progress >= view) {
                        window.cancelAnimationFrame(stepID)
                        progress = 0
                        //初始化起始时间
                        start = null
                        dom.style.transform = 'translateY(' + -progress + 'px)'
                    }
                }
    
            if(+window.getComputedStyle(dom).height.slice(0, -2) > view && this.state.tabVisible){
                interval = setInterval(() => {
                    mstep = window.requestAnimationFrame(step)
                }, 4000)
            }else {
                clearInterval(interval)
                window.cancelAnimationFrame(mstep)
            }
    
        ……
    
    第 2 条附言  ·  2019-04-22 16:42:15 +08:00

    上面提到的bug修复了,贴一下代码,字数限制删了注释。可以结合上面看一下

        scroll = () => {
            let dom = document.getElementById('scroll'),
                fragment = document.createDocumentFragment(),
                view = +window.getComputedStyle(dom.parentNode).height.slice(0, -2),
                li = +window.getComputedStyle(dom.firstElementChild).height.slice(0, -2),
                start = null, stepID = null, interval = null,
                step = timestamp => {
                    let liArr = [], i = 0;
                    while (i < Math.ceil(view / li)) {
                        liArr.push(dom.children[i])
                        i++
                    }
                    if (!start) start = timestamp
                    let progress = (timestamp - start) / 2;
                    dom.style.transform = 'translateY(' + -progress + 'px)'
                    stepID = window.requestAnimationFrame(step)
                    if (progress >= view) {
                        window.cancelAnimationFrame(stepID)
                        progress = 0
                        start = null
                        dom.style.transform = 'translateY(' + -progress + 'px)'
                        liArr.map(item => {
                            fragment.appendChild(item)
                            return item
                        })
                        dom.appendChild(fragment)
                    }
                }
            if(+window.getComputedStyle(dom).height.slice(0, -2) > view){
                interval = setInterval(() => {
                    window.requestAnimationFrame(step)
                }, 4000)
            }
            document.addEventListener("visibilitychange", function() {
                clearInterval(interval)
                if(!document.hidden) {
                    interval = setInterval(() => {
                        window.requestAnimationFrame(step)
                    }, 4000)
                }
            });
        }
    
    第 3 条附言  ·  2019-04-22 17:28:38 +08:00

    接上 在修改的时候朋友提到可以用transition来做动画,后来就用transition试了一下,也一起贴上来吧。因为transition要分别写多个浏览器的监听,这里偷懒只按需求写了webkit,在其他浏览器可能会有问题。transition相比rAF应该更简单,不过因为没用过rAF写了一遍也有收获。至于计时用interval还是timeout还是看实际情况,这里感觉interval少调用一次会好一点。感谢一直指导的 @ChefIsAwesome

        scroll = () => {
            let dom = document.getElementById('scroll'),
                //虚拟dom
                fragment = document.createDocumentFragment(),
                //获取精确值
                view = +window.getComputedStyle(dom.parentNode).height.slice(0, -2),
                li = +window.getComputedStyle(dom.firstElementChild).height.slice(0, -2),
                interval = null,
                fun = () => {
                    dom.style.transition = 'all 0.5s'
                    dom.style.transform = 'translateY(' + -view + 'px)'
                }
            if(+window.getComputedStyle(dom).height.slice(0, -2) > view){
                interval = setInterval(fun, 4000)
            }
            dom.addEventListener('webkitTransitionEnd', function () {
                dom.style.transition = 'all 0s'
                let liArr = [], i = 0
                while (i < Math.ceil(view / li)) {
                    liArr.push(dom.children[i])
                    i++
                }
                liArr.map(item => {
                    fragment.appendChild(item)
                    return item
                })
                dom.appendChild(fragment)
                dom.style.transform = 'translateY(0px)'
            }, false);
            document.addEventListener("visibilitychange", function() {
                clearInterval(interval)
                if(!document.hidden) {
                    interval = setInterval(fun, 4000)
                }
            });
        }
    
    5 条回复    2019-04-19 23:40:57 +08:00
    ChefIsAwesome
        1
    ChefIsAwesome  
       2019-04-17 16:28:20 +08:00 via Android   ❤️ 1
    两个尝试的方向:
    1 时间不对,应该用 raf
    2 滚动的时候用 translate y
    kingsleydon
        2
    kingsleydon  
       2019-04-17 16:39:59 +08:00
    建议直接用库,滚动动画是很多动画库都有的基础功能,react-spring react-motion
    leaveeel
        3
    leaveeel  
    OP
       2019-04-17 16:40:42 +08:00
    @ChefIsAwesome
    谢谢提供思路,raf 还没用过,我去试一下。
    用 margin 来做偏移也是习惯了,我再优化一下,感谢建议
    leaveeel
        4
    leaveeel  
    OP
       2019-04-19 10:45:49 +08:00
    @kingsleydon 谢谢,后面会尝试一下。一直都不习惯找库,看完需求就直接撸:XD。有的东西写出来挺爽的就是花时间


    @ChefIsAwesome 尝试用了 rAF 基本都解决了,就是当页面在后台的时候再切回来会一直执行动画到当前时间,试了用 visibilityState 控制也没用,就是 `if(this.state.tabVisible)` 这段。是不是方法写的有问题,主要代码在附言里,有空的话能再帮我看看吗。
    ChefIsAwesome
        5
    ChefIsAwesome  
       2019-04-19 23:40:57 +08:00
    @leaveeel 现在的浏览器为了省电,定时器在后台是停止的,恢复的时候一下子就把累计的定时器跑完。
    你可以:
    1.尝试不用 setInterval,用 setTimeout,类似递归的方式来写。
    loop = () => { if(shouldLoop) setTimeout(loop, delay) };
    2.尝试 debounce。这种短时间运行一堆东西,其实只有最后一个是有用的情况需要做 debounce。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5229 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 08:37 · PVG 16:37 · LAX 00:37 · JFK 03:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.