深刻理解requestAnimationFrame

原文  http://www.cnblogs.com/chaogex/p/3960175.html

前言

本文主要参考w3c资料,从底层实现原理的角度介绍了requestAnimationFrame、cancelAnimationFrame,给出了相关的示例代码以及我对实现原理的理解和讨论。javascript

本文介绍

浏览器中动画有两种实现形式:经过申明元素实现(如SVG中的php

元素)和脚本实现。html

能够经过setTimeout和setInterval方法来在脚本中实现动画,可是这样效果可能不够流畅,且会占用额外的资源。可参考《Html5 Canvas核心技术》中的论述:html5

 

 

它们有以下的特征:java

一、即便向其传递毫秒为单位的参数,它们也不能达到ms的准确性。这是由于javascript是单线程的,可能会发生阻塞。web

二、没有对调用动画的循环机制进行优化。api

三、没有考虑到绘制动画的最佳时机,只是一味地以某个大体的事件间隔来调用循环。浏览器

其实,使用setInterval或setTimeout来实现主循环,根本错误就在于它们抽象等级不符合要求。咱们想让浏览器执行的是一套能够控制各类细节的api,实现如“最优帧速率”、“选择绘制下一帧的最佳时机”等功能。可是若是使用它们的话,这些具体的细节就必须由开发者本身来完成。app

 

requestAnimationFrame不须要使用者指定循环间隔时间,浏览器会基于当前页面是否可见、CPU的负荷状况等来自行决定最佳的帧速率,从而更合理地使用CPU。dom

本文主要内容

  • 名词说明
  • API接口
  • 处理模型
  • 已解决的问题
  • 注意事项
  • 参考资料

名词说明

  • 动画帧请求回调函数列表

每一个Document都有一个动画帧请求回调函数列表,该列表能够当作是由< handle, callback>元组组成的集合。其中handle是一个整数,惟一地标识了元组在列表中的位置;callback是一个无返回值的、形参为一个时间值的函数(该时间值为由浏览器传入的从1970年1月1日到当前所通过的毫秒数)。 刚开始该列表为空。

  • Document

Dom模型中定义的Document节点。

  • Active document

浏览器上下文browsingContext中的Document被指定为active document。

  • browsingContext

    浏览器上下文。

浏览器上下文是呈现document对象给用户的环境。 浏览器中的1个tab或一个窗口包含一个顶级浏览器上下文,若是该页面有iframe,则iframe中也会有本身的浏览器上下文,称为嵌套的浏览器上下文。

  • DOM模型

详见个人理解DOM。

  • document对象

当html文档加载完成后,浏览器会建立一个document对象。它对应于Document节点,实现了HTML的Document接口。 经过该对象可得到整个html文档的信息,从而对HTML页面中的全部元素进行访问和操做。

  • HTML的Document接口

该接口对DOM定义的Document接口进行了扩展,定义了 HTML 专用的属性和方法。

详见 The Document object

  • 页面可见

当页面被最小化或者被切换成后台标签页时,页面为不可见,浏览器会触发一个 visibilitychange事件,并设置document.hidden属性为true;切换到显示状态时,页面为可见,也一样触发一个 visibilitychange事件,设置document.hidden属性为false。

详见 Page Visibility 、 Page Visibility(页面可见性) API介绍、微拓展

  • 队列

浏览器让一个单线程共用于执行javascrip和更新用户界面。这个线程一般被称为“浏览器UI线程”。 浏览器UI线程的工做基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被从新提取出来并运行。这些任务要么是运行javascript代码,要么执行UI更新,包括重绘和重排。

API接口

Window对象定义了如下两个接口:

partial interface Window { long requestAnimationFrame(FrameRequestCallback callback); void cancelAnimationFrame(long handle); };

requestAnimationFrame

requestAnimationFrame方法用于通知浏览器重采样动画。

当requestAnimationFrame(callback)被调用时不会执行callback,而是会将元组< handle,callback>插入到动画帧请求回调函数列表末尾(其中元组的callback就是传入requestAnimationFrame的回调函数),而且返回handle值,该值为浏览器定义的、大于0的整数,惟一标识了该回调函数在列表中位置。

每一个回调函数都有一个布尔标识cancelled,该标识初始值为false,而且对外不可见。

在后面的“处理模型” 中咱们会看到,浏览器在执行“采样全部动画”的任务时会遍历动画帧请求回调函数列表,判断每一个元组的callback的cancelled,若是为false,则执行callback。

cancelAnimationFrame

cancelAnimationFrame 方法用于取消先前安排的一个动画帧更新的请求。

当调用cancelAnimationFrame(handle)时,浏览器会设置该handle指向的回调函数的cancelled为true。

不管该回调函数是否在动画帧请求回调函数列表中,它的cancelled都会被设置为true。

若是该handle没有指向任何回调函数,则调用cancelAnimationFrame 不会发生任何事情。

处理模型

当页面可见而且动画帧请求回调函数列表不为空时,浏览器会按期地加入一个“采样全部动画”的任务到UI线程的队列中。

此处使用伪代码来讲明“采样全部动画”任务的执行步骤:

var list = {}; var browsingContexts = 浏览器顶级上下文及其下属的浏览器上下文; for (var browsingContext in browsingContexts) {  var time = 从1970年1月1日到当前所通过的毫秒数;  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) {    }   }  } }

已解决的问题

  • 为何在callback内部执行cancelAnimationFrame不能取消动画?

问题描述

以下面的代码会一直执行a:

 var id = null;  function a(time) {   console.log("animation");   window.cancelAnimationFrame(id); //不起做用   id = window.requestAnimationFrame(a);  }  a();

缘由分析

咱们来分析下这段代码是如何执行的:

一、执行a

(1)执行“a();”,执行函数a;

(2)执行“console.log("animation");”,打印“animation”;

(3)执行“window.cancelAnimationFrame(id);”,由于id为null,浏览器在动画帧请求回调函数列表中找不到对应的callback,因此不发生任何事情;

(4)执行“id = window.requestAnimationFrame(a);”,浏览器会将一个元组< handle, a>插入到Document的动画帧请求回调函数列表末尾,将id赋值为该元组的handle值;

二、a执行完毕后,执行第一个“采样全部动画”的任务

假设当前页面一直可见,由于动画帧请求回调函数列表不为空,因此浏览器会按期地加入一个“采样全部动画”的任务到线程队列中。

a执行完毕后的第一个“采样全部动画”的任务执行时会进行如下步骤:

(1)拷贝Document的动画帧请求回调函数列表到list变量中,清空Document的动画帧请求回调函数列表;

(2)遍历list的列表,列表有1个元组,该元组的callback为a;

(3)判断a的cancelled,为默认值false,因此执行a;

(4)执行“console.log("animation");”,打印“animation”;

(5)执行“window.cancelAnimationFrame(id);”,此时id指向当前元组的a(即当前正在执行的a),浏览器将

当前元组

的a的cancelled设为true。

 

(6)执行“id = window.requestAnimationFrame(a);”,浏览器会将

新的元组< handle, a>

插入到Document的动画帧请求回调函数列表末尾(新元组的a的cancelled为默认值false),将id赋值为该元组的handle值。

三、执行下一个“采样全部动画”的任务

当下一个“采样全部动画”的任务执行时,会判断动画帧请求回调函数列表的元组的a的cancelled,由于该元组为新插入的元组,因此值为默认值false,所以会继续执行a。

如此类推,浏览器会一直循环执行a。

解决方案

有下面两个方案:

一、执行requestAnimationFrame以后再执行cancelAnimationFrame。

下面代码只会执行一次a:

 var id = null;  function a(time) {   console.log("animation");   id = window.requestAnimationFrame(a);   window.cancelAnimationFrame(id);  }  a();

二、在callback外部执行cancelAnimationFrame。 下面代码只会执行一次a:

function a(time) { console.log("animation"); id = window.requestAnimationFrame(a); } a(); window.cancelAnimationFrame(id);

由于执行“window.cancelAnimationFrame(id);”时,id指向了新插入到动画帧请求回调函数列表中的元组的a,因此 “采样全部动画”任务判断元组的a的cancelled时,该值为true,从而再也不执行a。

注意事项

一、在处理模型 中咱们已经看到,在遍历执行拷贝的动画帧请求回调函数列表中的回调函数以前,Document的动画帧请求回调函数列表已经被清空了。所以若是要屡次执行回调函数,须要在回调函数中再次调用requestAnimationFrame将包含回调函数的元组加入到Document的动画帧请求回调函数列表中,从而浏览器才会再次按期加入“采样全部动画”的任务(当页面可见而且动画帧请求回调函数列表不为空时,浏览器才会加入该任务),执行回调函数。

例以下面代码只执行1次animate函数:

var id = null; function animate(time) { console.log("animation"); } window.requestAnimationFrame(animate);

下面代码会一直执行animate函数:

var id = null; function animate(time) { console.log("animation"); window.requestAnimationFrame(animate); } animate();

二、若是在执行回调函数或者Document的动画帧请求回调函数列表被清空以前屡次调用requestAnimationFrame插入同一个回调函数,那么列表中会有多个元组指向该回调函数(它们的handle不一样,但callback都为该回调函数),“采集全部动画”任务会执行屡次该回调函数。

例以下面的代码在执行“id1 = window.requestAnimationFrame(animate);”和“id2 = window.requestAnimationFrame(animate);”时会将两个元组(handle分别为id一、id2,回调函数callback都为animate)插入到Document的动画帧请求回调函数列表末尾。 由于“采样全部动画”任务会遍历执行动画帧请求回调函数列表的每一个回调函数,因此在“采样全部动画”任务中会执行两次animate。

 //下面代码会打印两次"animation"   var id1 = null,   id2 = null;  function animate(time) {   console.log("animation");  }   id1 = window.requestAnimationFrame(animate);  id2 = window.requestAnimationFrame(animate); //id1和id2值不一样,指向列表中不一样的元组,这两个元组中的callback都为同一个animate

兼容性方法

下面为《HTML5 Canvas 核心技术》给出的兼容主流浏览器的requestNextAnimationFrame 和cancelNextRequestAnimationFrame方法,你们可直接拿去用:

window.requestNextAnimationFrame = (function () {  var originalWebkitRequestAnimationFrame = undefined,    wrapper = undefined,    callback = undefined,    geckoVersion = 0,    userAgent = navigator.userAgent,    index = 0,    self = this;  // Workaround for Chrome 10 bug where Chrome  // does not pass the time to the animation function  if (window.webkitRequestAnimationFrame) {   // Define the wrapper   wrapper = function (time) {    if (time === undefined) {     time = +new Date();    }    self.callback(time);   };   // Make the switch   originalWebkitRequestAnimationFrame = window.webkitRequestAnimationFrame;   window.webkitRequestAnimationFrame = function (callback, element) {    self.callback = callback;    // Browser calls the wrapper and wrapper calls the callback    originalWebkitRequestAnimationFrame(wrapper, element);   }  }  // Workaround for Gecko 2.0, which has a bug in  // mozRequestAnimationFrame() that restricts animations  // to 30-40 fps.  if (window.mozRequestAnimationFrame) {   // Check the Gecko version. Gecko is used by browsers   // other than Firefox. Gecko 2.0 corresponds to   // Firefox 4.0.   index = userAgent.indexOf('rv:');   if (userAgent.indexOf('Gecko') != -1) {    geckoVersion = userAgent.substr(index + 3, 3);    if (geckoVersion === '2.0') {     // Forces the return statement to fall through     // to the setTimeout() function.     window.mozRequestAnimationFrame = undefined;    }   }  }  return window.requestAnimationFrame ||    window.webkitRequestAnimationFrame ||    window.mozRequestAnimationFrame ||    window.oRequestAnimationFrame ||    window.msRequestAnimationFrame ||    function (callback, element) {     var start,       finish;     window.setTimeout(function () {      start = +new Date();      callback(start);      finish = +new Date();      self.timeout = 1000 / 60 - (finish - start);     }, self.timeout);    }; }());  window.cancelNextRequestAnimationFrame = window.cancelRequestAnimationFrame   || window.webkitCancelAnimationFrame   || window.webkitCancelRequestAnimationFrame   || window.mozCancelRequestAnimationFrame   || window.oCancelRequestAnimationFrame   || window.msCancelRequestAnimationFrame   || clearTimeout;
相关文章
相关标签/搜索