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

requestAnimationFrame 方法你真的用对了吗?

  •  
  •   joe2016 · 2017-07-18 10:38:43 +08:00 · 3171 次点击
    这是一个创建于 2666 天前的主题,其中的信息可能已经有所发展或是发生改变。

    requestAnimationFrame 方法你真的用对了吗?

    requestAnimationFrame 方法让我们可以在下一帧开始时调用指定函数。但是很多人可能不知道,不管三七二十一直接在 requestAnimationFrame 的回调函数里绘制动画会有一个问题。是什么问题呢?要理解这个问题,我们先要了解 requestAnimationFrame 的一个知识点。

    requestAnimationFrame 不管理回调函数

    这个知识点就是 requestAnimationFrame 不管理回调函数。这一点在 w3c 中明确说明了。

    Also note that multiple calls to requestAnimationFrame with the same callback (before callbacks are invoked and the list is cleared) will result in multiple entries being in the list with that same callback, and thus will result in that callback being invoked more than once for the animation frame.
    w3c

    即在回调被执行前,多次调用带有同一回调函数的 requestAnimationFrame,会导致回调在同一帧中执行多次。我们可以通过一个简单的例子模拟在同一帧内多次调用 requestAnimationFrame 的场景:

    const animation = timestamp => console.log('animation called at', timestamp)
    
    window.requestAnimationFrame(animation)
    window.requestAnimationFrame(animation)
    // animation called at 320.7559999991645
    // animation called at 320.7559999991645
    

    我们用连续调用两次 requestAnimationFrame 模拟在同一帧中调用两次 requestAnimationFrame

    例子中的 timestamp 是由 requestAnimationFrame 传给回调函数的,表示回调队列被触发的时间。由输出可知,animation 函数在同一帧内被执行了两次,即绘制了两次动画。然而在同一帧绘制两次动画很明显是多余的,相当于画了一幅画,然后再在这幅画上再画上同样的一幅画。

    问题

    那么什么场景下,requestAnimationFrame 会在一帧内被多次调用呢?熟悉事件的同学应该马上能想到 mousemove, scroll 这类事件。

    所以前面我们提到的问题就是:因为 requestAnimationFrame 不管理回调函数,在滚动、触摸这类高触发频率的事件回调里,如果调用 requestAnimationFrame 然后绘制动画,可能会造成多余的计算和绘制。例如:

    window.addEventListener('scroll', e => {
        window.requestAnimationFrame(timestamp => {
            animation(timestamp)
        })
    })
    

    在上面代码中,scroll 事件可能在一帧内多次触发,所以 animation 函数可能会在一帧内重复绘制,造成不必要的计算和渲染。

    解决方法

    对于这种高频发事件,一般的解决方法是使用节流函数。但是在这里使用节流函数并不能完美解决问题。因为节流函数是通过时间管理队列的,而 requestAnimationFrame 的触发时间是不固定的,在高刷新频率的显示屏上时间会小于 16.67ms,页面如果被推入后台,时间可能大于 16.67ms。

    完美的解决方案是通过 requestAnimationFrame 来管理队列,其思路就是保证 requestAnimationFrame 的队列里,同样的回调函数只有一个。示意代码如下:

    const onScroll = e => {
        if (scheduledAnimationFrame) { return }
    
        scheduledAnimationFrame = true
        window.requestAnimationFrame(timestamp => {
            scheduledAnimationFrame = false
            animation(timestamp)
        })
    }
    window.addEventListener('scroll', onScroll)
    

    但是每次都要写这么一堆代码,也有点麻烦。所以我开源了 raf-plus 库用于解决这个问题,有需要的的同学可以用用~

    结论

    requestAnimationFrame 不管理回调函数队列,而滚动、触摸这类高触发频率事件的回调可能会在同一帧内触发多次。所以正确使用 requestAnimationFrame 的姿势是,在同一帧内可能调用多次 requestAnimationFrame 时,要管理回调函数,防止重复绘制动画。

    2 条回复    2017-07-19 09:03:41 +08:00
    MinonHeart
        1
    MinonHeart  
       2017-07-18 22:21:21 +08:00 via iPhone
    这是防止回调函数堆积,类似丢帧的操作
    joe2016
        2
    joe2016  
    OP
       2017-07-19 09:03:41 +08:00
    @MinonHeart 不是的,防止回调函数堆积,重复绘制。比如说 requestAnimationFrame 队列中有 2 个一模一样的动画函数,那么就会在一帧内绘制两次动画,而一帧内绘制 2 次就是重复绘制了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   943 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 21:29 · PVG 05:29 · LAX 13:29 · JFK 16:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.