你知道的requestAnimationFrame【从0到0.1】

随着技术与设备的发展,用户的终端对动画的表现能力愈来愈强,更多的场景开始大量使用动画。在 Web 应用中,实现动画效果的方法比较多,JavaScript 中能够经过定时器 setTimeout 来实现,css3 可使用 transitionanimation 来实现,html5 中的 canvas 也能够实现。除此以外,html5 还提供一个专门用于请求动画的 API,即 requestAnimationFramejavascript

本文内容均非原创,而是在知识点的收集与搬运中学习与理解,也欢迎你们收集与搬运本篇文章!css

1. 是什么

  • HTML5 新增长的 API,相似于 setTimeout 定时器html

  • window 对象的一个方法,window.requestAnimationFramehtml5

    partial interface Window {
      long requestAnimationFrame(FrameRequestCallback callback);
      void cancelAnimationFrame(long handle);
    };
    复制代码
  • 浏览器(因此只能在浏览器中使用)专门为动画提供的 API,让 DOM 动画、Canvas 动画、SVG 动画、WebGL 动画等有一个统一的刷新机制java

2. 作什么

  • 浏览器重绘频率通常会和显示器的刷新率保持同步。大多数浏览器采起 W3C 规范的建议,浏览器的渲染页面的标准帧率也为 60FPS(frames/ per second)
  • 按帧对网页进行重绘。该方法告诉浏览器但愿执行动画并请求浏览器在下一次重绘以前调用回调函数来更新动画css3

  • 由系统来决定回调函数的执行时机,在运行时浏览器会自动优化方法的调用git

    • 显示器有固定的刷新频率(60Hz 或 75Hz),也就是说,每秒最多只能重绘 60 次或 75 次,requestAnimationFrame 的基本思想让页面重绘的频率与这个刷新频率保持同步github

      好比显示器屏幕刷新率为 60Hz,使用requestAnimationFrame API,那么回调函数就每1000ms / 60 ≈ 16.7ms执行一次;若是显示器屏幕的刷新率为 75Hz,那么回调函数就每1000ms / 75 ≈ 13.3ms执行一次。web

    • 经过requestAnimationFrame调用回调函数引发的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同。因此 requestAnimationFrame 不须要像setTimeout那样传递时间间隔,而是浏览器经过系统获取并使用显示器刷新频率canvas

      好比一个动画,宽度从 0px 加一递增到 100px。无缓动效果的状况下,浏览器重绘一次,宽度就加 1。

3. 用法

动画帧请求回调函数列表:每一个 Document 都有一个动画帧请求回调函数列表,该列表能够当作是由<handle, callback>元组组成的集合。

  • handle 是一个整数,惟一地标识了元组在列表中的位置,cancelAnimationFrame()能够经过它中止动画
  • callback 是一个无返回值的、形参为一个时间值的函数(该时间值为由浏览器传入的从 1970 年 1 月 1 日到当前所通过的毫秒数)。
  • 刚开始该列表为空。

页面可见性 API

  • 当页面被最小化或者被切换成后台标签页时,页面为不可见,浏览器会触发一个visibilitychange事件,并设置document.hidden属性为true
  • 当页面切换到显示状态,页面变为可见,同时触发一个visibilitychange事件,设置document.hidden属性为false
  • 调用操做。与setTimeout类似,可是不须要设置间隔时间,使用一个回调函数做为参数,返回一个大于 0 的整数

    handle = requestAnimationFrame(callback);
    复制代码
    • 参数callback,是一个回调函数,在下次从新绘制动画时调用。该回调函数接收惟一参数,是一个高精度时间戳(performance.now()),指触发回调函数的当前时间(不用手动传入)
    • 返回值是一个long型的非零整数,是requestAnimationFrame回调函数列表中惟一的标识,表示定时器的编号,无其余意义
  • 取消操做

    cancelAnimationFrame(handle);
    复制代码
    • 参数是调用requestAnimationFrame时的返回值
    • 取消操做没有返回值
  • 浏览器执行过程

    • 首先判断document.hidden属性是否为true(页面是否可见),页面处于可见状态才会执行后面步骤

    • 浏览器清空上一轮的动画函数

    • requestAnimationFrame将回调函数追加到动画帧请求回调函数列表的末尾

      当执行requestAnimationFrame(callback)的时候,不会当即调用 callback 函数,只是将其放入队列。每一个回调函数都有一个布尔标识cancelled,该标识初始值为false,而且对外不可见。

    • 当浏览器再执行列表中的回调函数的时候,判断每一个元组的 callback 的cancelled,若是为false,则执行 callback

      当页面可见而且动画帧请求回调函数列表不为空,浏览器会按期将这些回调函数加入到浏览器 UI 线程的队列中

    • 博客园上yyc 元超的文章深刻理解 requestAnimationFrame中提供了让一个伪代码,用来讲明“采样全部动画”任务的执行步骤

      var list = {};
      var browsingContexts = 浏览器顶级上下文及其下属的浏览器上下文;
      for (var browsingContext in browsingContexts) {
      /* !将时间值从 DOMTimeStamp 更改成 DOMHighResTimeStamp 是 W3C 针对基于脚本动画计时控制规范的最新编辑草案中的最新更改, * 而且某些供应商仍将其做为 DOMTimeStamp 实现。 * 较早版本的 W3C 规范使用 DOMTimeStamp,容许你将 Date.now 用于当前时间。 * 如上所述,某些浏览器供应商可能仍实现 DOMTimeStamp 参数,或者还没有实现 window.performance.now 计时函数。 * 所以须要用户进行polyfill */
          var time = DOMHighResTimeStamp   //从页面导航开始时测量的高精确度时间。DOMHighResTimeStamp 以毫秒为单位,精确到千分之一毫秒。此时间值不直接与 Date.now() 进行比较,后者测量自 1970 年 1 月 1 日至今以毫秒为单位的时间。若是你但愿将 time 参数与当前时间进行比较,请使用当前时间的 window.performance.now。
        var d = browsingContext 的 active document;   //即当前浏览器上下文中的Document节点
          //若是该active document可见
          if (d.hidden !== true) {
              //拷贝 active document 的动画帧请求回调函数列表到 list 中,并清空该列表
              var doclist = d的动画帧请求回调函数列表
              doclist.appendTo(list);
              clear(doclist);
          }
          //遍历动画帧请求回调函数列表的元组中的回调函数
          for (var callback in list) {
              if (callback.cancelled !== true) {
                  try {
                      //每一个 browsingContext 都有一个对应的 WindowProxy 对象,WindowProxy 对象会将 callback 指向 active document 关联的 window 对象。
                      //传入时间值time
                      callback.call(window, time);
                  }
                  //忽略异常
                  catch (e) {
                  }
              }
          }
      }
      复制代码
    • 当调用cancelAnimationFrame(handle)时,浏览器会设置该 handle 指向的回调函数的cancelledtrue(不管该回调函数是否在动画帧请求回调函数列表中)。若是该 handle 没有指向任何回调函数,则什么也不会发生。

  • 递归调用。要想实现一个完整的动画,应该在回调函数中递归调用回调函数

    let count = 0;
    let rafId = null;
    /** * 回调函数 * @param time requestAnimationFrame 调用该函数时,自动传入的一个时间 */
    function requestAnimation(time) {
      console.log(time);
      // 动画没有执行完,则递归渲染
      if (count < 50) {
        count++;
        // 渲染下一帧
        rafId = requestAnimationFrame(requestAnimation);
      }
    }
    // 渲染第一帧
    requestAnimationFrame(requestAnimation);
    复制代码
  • 若是在执行回调函数或者 Document 的动画帧请求回调函数列表被清空以前屡次调用 requestAnimationFrame 调用同一个回调函数,那么列表中会有多个元组指向该回调函数(它们的 handle 不一样,但 callback 都为该回调函数),“采集全部动画”任务会执行屡次该回调函数。(类比定时器setTimeout

    function counter() {
      let count = 0;
      function animate(time) {
        if (count < 50) {
          count++;
          console.log(count);
          requestAnimationFrame(animate);
        }
      }
      requestAnimationFrame(animate);
    }
    btn.addEventListener("click", counter, false);
    复制代码
    • 屡次点击按钮,会发现打印出来多个序列数值(下图中,连续触发三次,打印了三个有序列)

      屡次调用回调函数

    • 若是是做用于动画,动画会出现突变的状况

4. 兼容性

来源:Polyfill for requestAnimationFrame/cancelAnimationFrame

在浏览器初次加载的时候执行下面的代码便可。

// 使用 Date.now 获取时间戳性能比使用 new Date().getTime 更高效
if (!Date.now)
  Date.now = function() {
    return new Date().getTime();
  };

(function() {
 "use strict";

  var vendors = ["webkit", "moz"];
  for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
    var vp = vendors[i];
    window.requestAnimationFrame = window[vp + "RequestAnimationFrame"];
    window.cancelAnimationFrame =
      window[vp + "CancelAnimationFrame"] ||
      window[vp + "CancelRequestAnimationFrame"];
  }
  // 上面方法都不支持的状况,以及IOS6的设备
  // 使用 setTimeout 模拟实现
  if (
    /iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) ||
    !window.requestAnimationFrame ||
    !window.cancelAnimationFrame
  ) {
    var lastTime = 0;
    // 和经过时间戳实现节流功能的函数类似
    window.requestAnimationFrame = function(callback) {
      var now = Date.now();
      var nextTime = Math.max(lastTime + 16, now);
      // 实际上第1帧是不许确的,首次nextTime - now = 0
      return setTimeout(function() {
        callback((lastTime = nextTime));
      }, nextTime - now);
    };
    window.cancelAnimationFrame = clearTimeout;
  }
})();
复制代码

5. 优点

requestAnimationFrame采用系统时间间隔,保持最佳绘制效率。不会由于间隔时间太短,形成过分绘制,增长开销;也不会由于间隔时间过长,使动画卡顿。

从实现的功能和使用方法上,requestAnimationFrame与定时器setTimeout都类似,因此说其优点是同setTimeout实现的动画相比。

a. 提高性能,防止掉帧

  • 浏览器 UI 线程:浏览器让执行 JavaScript 和更新用户界面(包括重绘和回流)共用同一个单线程,称为“浏览器 UI 线程”
  • 浏览器 UI 线程的工做基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被从新提取出来并运行。这些任务要么是运行 JavaScript 代码,要么执行 UI 更新。
  • 经过setTimeout实现动画

    • setTimeout经过设置一个间隔时间不断改变图像,达到动画效果。该方法在一些低端机上会出现卡顿、抖动现象。这种现象通常有两个缘由:

      • setTimeout的执行时间并非肯定的。

        在 JavaScript 中,setTimeout任务被放进异步队列中,只有当主线程上的任务执行完之后,才会去检查该队列的任务是否须要开始执行。因此,setTimeout的实际执行时间通常比其设定的时间晚一些。这种运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到【浏览器 UI 线程队列】中以等待执行的时间。若是队列前面已经加入了其余任务,那动画代码就要等前面的任务完成后再执行

        let startTime = performance.now();
        setTimeout(() => {
          let endTime = performance.now();
          console.log(endTime - startTime);
        }, 50);
        /* 一个很是耗时的任务 */
        for (let i = 0; i < 20000; i++) {
          console.log(0);
        }
        复制代码

        定时器

      • 刷新频率受屏幕分辨率和屏幕尺寸影响,不一样设备的屏幕刷新率可能不一样,setTimeout只能设置固定的时间间隔,这个时间和屏幕刷新间隔可能不一样

    • 以上两种状况都会致使setTimeout的执行步调和屏幕的刷新步调不一致,从而引发丢帧现象。

      • setTimeout的执行只是在内存中对图像属性进行改变,这个改变必需要等到下次浏览器重绘时才会被更新到屏幕上。若是和屏幕刷新步调不一致,就可能致使中间某些帧的操做被跨越过去,直接更新下下一帧的图像。

        假如使用定时器设置间隔 10ms 执行一个帧,而浏览器刷新间隔是 16.6ms(即 60FPS)

        丢帧

        由图可知,在 20ms 时,setTimeout调用回调函数在内存中将图像的属性进行了修改,可是此时浏览器下次刷新是在 33.2ms 的时候,因此 20ms 修改的图像没有更新到屏幕上。 而到了 30ms 的时候,setTimeout又一次调用回调函数并改变了内存中图像的属性,以后浏览器就刷新了,20ms 更新的状态被 30ms 的图像覆盖了,屏幕上展现的是 30ms 时的图像,因此 20ms 的这一帧就丢失了。丢失的帧多了,画面就卡顿了。

  • 使用 requestAnimationFrame 执行动画,最大优点是能保证回调函数在屏幕每一次刷新间隔中只被执行一次,这样就不会引发丢帧,动画也就不会卡顿

b. 节约资源,节省电源

  • 使用 setTimeout 实现的动画,当页面被隐藏或最小化时,定时器setTimeout仍在后台执行动画任务,此时刷新动画是彻底没有意义的(实际上 FireFox/Chrome 浏览器对定时器作了优化:页面闲置时,若是时间间隔小于 1000ms,则中止定时器,与requestAnimationFrame行为相似。若是时间间隔>=1000ms,定时器依然在后台执行)

    // 在浏览器开发者工具的Console页执行下面代码。
    // 当开始输出count后,切换浏览器tab页,再切换回来,能够发现打印的值没有中止,甚至可能已经执行完了
    let count = 0;
    let timer = setInterval(() => {
      if (count < 20) {
        count++;
        console.log(count);
      } else {
        clearInterval(timer);
        timer = null;
      }
    }, 2000);
    复制代码
  • 使用requestAnimationFrame,当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停,因为requestAnimationFrame保持和屏幕刷新同步执行,因此也会被暂停。当页面被激活时,动画从上次停留的地方继续执行,节约 CPU 开销。

    // 在浏览器开发者工具的Console页执行下面代码。
    // 当开始输出count后,切换浏览器tab页,再切换回来,能够发现打印的值从离开前的值继续输出
    let count = 0;
    function requestAnimation() {
      if (count < 500) {
        count++;
        console.log(count);
        requestAnimationFrame(requestAnimation);
      }
    }
    requestAnimationFrame(requestAnimation);
    复制代码

c. 函数节流

  • 一个刷新间隔内函数执行屡次时没有意义的,由于显示器每 16.7ms 刷新一次,屡次绘制并不会在屏幕上体现出来
  • 在高频事件(resizescroll等)中,使用requestAnimationFrame能够防止在一个刷新间隔内发生屡次函数执行,这样保证了流畅性,也节省了函数执行的开销
  • 某些状况下能够直接使用requestAnimationFrame替代 Throttle 函数,都是限制回调函数执行的频率

6. 应用

  • 简单的进度条动画

    function loadingBar(ele) {
      // 使用闭包保存定时器的编号
      let handle;
      return () => {
        // 每次触发将进度清空
        ele.style.width = "0";
        // 开始动画前清除上一次的动画定时器
        // 不然会开启多个定时器
        cancelAnimationFrame(handle);
        // 回调函数
        let _progress = () => {
          let eleWidth = parseInt(ele.style.width);
          if (eleWidth < 200) {
            ele.style.width = `${eleWidth + 5}px`;
            handle = requestAnimationFrame(_progress);
          } else {
            cancelAnimationFrame(handle);
          }
        };
        handle = requestAnimationFrame(_progress);
      };
    }
    复制代码
  • 添加缓动效果,实现一个元素块按照三阶贝塞尔曲线的ease-in-out缓动特效参数运动。如何使用 Javascript 实现缓动特效

缓动动画:指定动画效果在执行时的速度,使其看起来更加真实。

/** * @param {HTMLElement} ele 元素节点 * @param {number} change 改变量 * @param {number} duration 动画持续时长 */
function moveBox(ele, change, duration) {
  // 使用闭包保存定时器标识
  let handle;
  // 返回动画函数
  return () => {
    // 开始时间
    let startTime = performance.now();
    // 防止启动多个定时器
    cancelAnimationFrame(handle);
    // 回调函数
    function _animation() {
      // 这一帧开始的时间
      let current = performance.now();
      let eleTop = ele.offsetLeft;
      // 这一帧内元素移动的距离
      let left = change * easeInOutCubic((current - startTime) / duration);
      ele.style.left = `${~~left}px`;
      // 判断动画是否执行完
      if ((current - startTime) / duration < 1) {
        handle = requestAnimationFrame(_animation);
      } else {
        cancelAnimationFrame(handle);
      }
    }
    // 第一帧开始
    handle = requestAnimationFrame(_animation);
  };
}
/** * 三阶贝塞尔曲线ease-in-out * @param {number} k */
function easeInOutCubic(k) {
  return (k *= 2) < 1 ? 0.5 * k * k * k : 0.5 * ((k -= 2) * k * k + 2);
}
复制代码

7. 相关

本文内容均非原创,而是在知识点的收集与搬运中学习与理解,也欢迎你们收集与搬运本篇文章!

  • https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
  • https://caniuse.com/#search=requestAnimationFrame
  • https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/
  • https://javascript.ruanyifeng.com/htmlapi/requestanimationframe.html
  • https://www.cnblogs.com/xiaohuochai/p/5777186.html
  • https://juejin.im/post/5b6020b8e51d4535253b30d1
  • https://www.cnblogs.com/chaogex/p/3960175.html#explain
  • http://www.softwhy.com/article-7204-1.html
  • https://easings.net/zh-cn#
  • https://zhuanlan.zhihu.com/p/25676357
  • https://www.cnblogs.com/onepixel/p/7078617.html
相关文章
相关标签/搜索