88bf必发唯一官网 3

浅析 requestAnimationFrame88bf必发唯一官网

浅析 requestAnimationFrame

2017/03/02 · JavaScript
· 1 评论 ·
requestAnimationFrame

原文出处: 淘宝前端团队(FED)-
腾渊
   

88bf必发唯一官网 1

相信现在绝大多数人在 JavaScript 中绘制动画已经在使用
requestAnimationFrame 了,关于 requestAnimationFrame
的种种就不多说了,关于这个 API 的资料,详见
http://www.w3.org/TR/animation-timing/,https://developer.mozilla.org/en/docs/Web/API/window.requestAnimationFrame。

如果我们把时钟往前拨到引入 requestAnimationFrame 之前,如果在 JavaScript
中要实现动画效果,怎么办呢?无外乎使用 setTimeout 或
setInterval。那么问题就来了:

  • 如何确定正确的时间间隔(浏览器、机器硬件的性能各不相同)?
  • 毫秒的不精确性怎么解决?
  • 如何避免过度渲染(渲染频率太高、tab 不可见等等)?

开发者可以用很多方式来减轻这些问题的症状,但是彻底解决,这个、基本、很难。

归根到底,问题的根源在于时机。对于前端开发者来说,setTimeout 和
setInterval 提供的是一个等长的定时器循环(timer
loop),但是对于浏览器内核对渲染函数的响应以及何时能够发起下一个动画帧的时机,是完全不了解的。对于浏览器内核来讲,它能够了解发起下一个渲染帧的合适时机,但是对于任何
setTimeout 和 setInterval
传入的回调函数执行,都是一视同仁的,它很难知道哪个回调函数是用于动画渲染的,因此,优化的时机非常难以掌握。悖论就在于,写
JavaScript
的人了解一帧动画在哪行代码开始,哪行代码结束,却不了解应该何时开始,应该何时结束,而在内核引擎来说,事情却恰恰相反,所以二者很难完美配合,直到
requestAnimationFrame 出现。

本人很喜欢 requestAnimationFrame 这个名字,因为起得非常直白 – request
animation frame,对于这个 API 最好的解释就是名字本身了。这样一个
API,你传入的 API 不是用来渲染一帧动画,你上街都不好意思跟人打招呼。

由于本人是个喜欢阅读代码的人,为了体现自己好学的态度,特意读了下 Chrome
的代码去了解它是怎么实现 requestAnimationFrame 的(代码基于 Android
4.4):

JavaScript

int
Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback>
callback) { if (!m_scriptedAnimationController) {
m_scriptedAnimationController =
ScriptedAnimationController::create(this); // We need to make sure that
we don’t start up the animation controller on a background tab, for
example. if (!page()) m_scriptedAnimationController->suspend(); }
return m_scriptedAnimationController->registerCallback(callback); }

1
2
3
4
5
6
7
8
9
10
11
int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback)
{
  if (!m_scriptedAnimationController) {
    m_scriptedAnimationController = ScriptedAnimationController::create(this);
    // We need to make sure that we don’t start up the animation controller on a background tab, for example.
      if (!page())
        m_scriptedAnimationController->suspend();
  }
 
  return m_scriptedAnimationController->registerCallback(callback);
}

仔细看看就觉得底层实现意外地简单,生成一个 ScriptedAnimationController
的实例,然后注册这个 callback。那我们就看看 ScriptAnimationController
里面做了些什么:

JavaScript

void ScriptedAnimationController::serviceScriptedAnimations(double
monotonicTimeNow) { if (!m_callbacks.size() || m_suspendCount) return;
double highResNowMs = 1000.0 *
m_document->loader()->timing()->monotonicTimeToZeroBasedDocumentTime(monotonicTimeNow);
double legacyHighResNowMs = 1000.0 *
m_document->loader()->timing()->monotonicTimeToPseudoWallTime(monotonicTimeNow);
// First, generate a list of callbacks to consider. Callbacks registered
from this point // on are considered only for the “next” frame, not this
one. CallbackList callbacks(m_callbacks); // Invoking callbacks may
detach elements from our document, which clears the document’s //
reference to us, so take a defensive reference.
RefPtr<ScriptedAnimationController> protector(this); for (size_t
i = 0; i < callbacks.size(); ++i) { RequestAnimationFrameCallback*
callback = callbacks[i].get(); if (!callback->m_firedOrCancelled)
{ callback->m_firedOrCancelled = true;
InspectorInstrumentationCookie cookie =
InspectorInstrumentation::willFireAnimationFrame(m_document,
callback->m_id); if (callback->m_useLegacyTimeBase)
callback->handleEvent(legacyHighResNowMs); else
callback->handleEvent(highResNowMs);
InspectorInstrumentation::didFireAnimationFrame(cookie); } } // Remove
any callbacks we fired from the list of pending callbacks. for (size_t
i = 0; i < m_callbacks.size();) { if
(m_callbacks[i]->m_firedOrCancelled) m_callbacks.remove(i); else
++i; } if (m_callbacks.size()) scheduleAnimation(); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void ScriptedAnimationController::serviceScriptedAnimations(double monotonicTimeNow)
{
  if (!m_callbacks.size() || m_suspendCount)
    return;
 
    double highResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToZeroBasedDocumentTime(monotonicTimeNow);
    double legacyHighResNowMs = 1000.0 * m_document->loader()->timing()->monotonicTimeToPseudoWallTime(monotonicTimeNow);
 
    // First, generate a list of callbacks to consider.  Callbacks registered from this point
    // on are considered only for the "next" frame, not this one.
    CallbackList callbacks(m_callbacks);
 
    // Invoking callbacks may detach elements from our document, which clears the document’s
    // reference to us, so take a defensive reference.
    RefPtr<ScriptedAnimationController> protector(this);
 
    for (size_t i = 0; i < callbacks.size(); ++i) {
        RequestAnimationFrameCallback* callback = callbacks[i].get();
      if (!callback->m_firedOrCancelled) {
        callback->m_firedOrCancelled = true;
        InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireAnimationFrame(m_document, callback->m_id);
        if (callback->m_useLegacyTimeBase)
          callback->handleEvent(legacyHighResNowMs);
        else
          callback->handleEvent(highResNowMs);
        InspectorInstrumentation::didFireAnimationFrame(cookie);
      }
    }
 
    // Remove any callbacks we fired from the list of pending callbacks.
    for (size_t i = 0; i < m_callbacks.size();) {
      if (m_callbacks[i]->m_firedOrCancelled)
        m_callbacks.remove(i);
      else
        ++i;
    }
 
    if (m_callbacks.size())
      scheduleAnimation();
}

这个函数自然就是执行回调函数的地方了。那么动画是如何被触发的呢?我们需要快速地看一串函数(一个从下往上的
call stack):

JavaScript

void PageWidgetDelegate::animate(Page* page, double
monotonicFrameBeginTime) { FrameView* view = mainFrameView(page); if
(!view) return;
view->serviceScriptedAnimations(monotonicFrameBeginTime); }

1
2
3
4
5
6
7
void PageWidgetDelegate::animate(Page* page, double monotonicFrameBeginTime)
{
  FrameView* view = mainFrameView(page);
  if (!view)
    return;
  view->serviceScriptedAnimations(monotonicFrameBeginTime);
}

JavaScript

void WebViewImpl::animate(double monotonicFrameBeginTime) {
TRACE_EVENT0(“webkit”, “WebViewImpl::animate”); if
(!monotonicFrameBeginTime) monotonicFrameBeginTime =
monotonicallyIncreasingTime(); // Create synthetic wheel events as
necessary for fling. if (m_gestureAnimation) { if
(m_gestureAnimation->animate(monotonicFrameBeginTime))
scheduleAnimation(); else { m_gestureAnimation.clear(); if
(m_layerTreeView) m_layerTreeView->didStopFlinging();
PlatformGestureEvent endScrollEvent(PlatformEvent::GestureScrollEnd,
m_positionOnFlingStart, m_globalPositionOnFlingStart, 0, 0, 0, false,
false, false, false);
mainFrameImpl()->frame()->eventHandler()->handleGestureScrollEnd(endScrollEvent);
} } if (!m_page) return; PageWidgetDelegate::animate(m_page.get(),
monotonicFrameBeginTime); if (m_continuousPaintingEnabled) {
ContinuousPainter::setNeedsDisplayRecursive(m_rootGraphicsLayer,
m_pageOverlays.get()); m_client->scheduleAnimation(); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void WebViewImpl::animate(double monotonicFrameBeginTime)
{
  TRACE_EVENT0("webkit", "WebViewImpl::animate");
 
  if (!monotonicFrameBeginTime)
      monotonicFrameBeginTime = monotonicallyIncreasingTime();
 
  // Create synthetic wheel events as necessary for fling.
  if (m_gestureAnimation) {
    if (m_gestureAnimation->animate(monotonicFrameBeginTime))
      scheduleAnimation();
    else {
      m_gestureAnimation.clear();
      if (m_layerTreeView)
        m_layerTreeView->didStopFlinging();
 
      PlatformGestureEvent endScrollEvent(PlatformEvent::GestureScrollEnd,
          m_positionOnFlingStart, m_globalPositionOnFlingStart, 0, 0, 0,
          false, false, false, false);
 
      mainFrameImpl()->frame()->eventHandler()->handleGestureScrollEnd(endScrollEvent);
    }
  }
 
  if (!m_page)
    return;
 
  PageWidgetDelegate::animate(m_page.get(), monotonicFrameBeginTime);
 
  if (m_continuousPaintingEnabled) {
    ContinuousPainter::setNeedsDisplayRecursive(m_rootGraphicsLayer, m_pageOverlays.get());
    m_client->scheduleAnimation();
  }
}

JavaScript

void RenderWidget::AnimateIfNeeded() { if
(!animation_update_pending_) return; // Target 60FPS if vsync is on.
Go as fast as we can if vsync is off. base::TimeDelta animationInterval
= IsRenderingVSynced() ? base::TimeDelta::FromMilliseconds(16) :
base::TimeDelta(); base::Time now = base::Time::Now(); //
animation_floor_time_ is the earliest time that we should animate
when // using the dead reckoning software scheduler. If we’re using
swapbuffers // complete callbacks to rate limit, we can ignore this
floor. if (now >= animation_floor_time_ ||
num_swapbuffers_complete_pending_ > 0) {
TRACE_EVENT0(“renderer”, “RenderWidget::AnimateIfNeeded”)
animation_floor_time_ = now + animationInterval; // Set a timer to
call us back after animationInterval before // running animation
callbacks so that if a callback requests another // we’ll be sure to run
it at the proper time. animation_timer_.Stop();
animation_timer_.Start(FROM_HERE, animationInterval, this,
&RenderWidget::AnimationCallback); animation_update_pending_ = false;
if (is_accelerated_compositing_active_ && compositor_) {
compositor_->Animate(base::TimeTicks::Now()); } else { double
frame_begin_time = (base::TimeTicks::Now() –
base::TimeTicks()).InSecondsF();
webwidget_->animate(frame_begin_time); } return; }
TRACE_EVENT0(“renderer”, “EarlyOut_AnimatedTooRecently”); if
(!animation_timer_.IsRunning()) { // This code uses base::Time::Now()
to calculate the floor and next fire // time because javascript’s Date
object uses base::Time::Now(). The // message loop uses base::TimeTicks,
which on windows can have a // different granularity than base::Time. //
The upshot of all this is that this function might be called before //
base::Time::Now() has advanced past the animation_floor_time_. To //
avoid exposing this delay to javascript, we keep posting delayed //
tasks until base::Time::Now() has advanced far enough. base::TimeDelta
delay = animation_floor_time_ – now;
animation_timer_.Start(FROM_HERE, delay, this,
&RenderWidget::AnimationCallback); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void RenderWidget::AnimateIfNeeded() {
  if (!animation_update_pending_)
    return;
 
  // Target 60FPS if vsync is on. Go as fast as we can if vsync is off.
  base::TimeDelta animationInterval = IsRenderingVSynced() ? base::TimeDelta::FromMilliseconds(16) : base::TimeDelta();
 
  base::Time now = base::Time::Now();
 
  // animation_floor_time_ is the earliest time that we should animate when
  // using the dead reckoning software scheduler. If we’re using swapbuffers
  // complete callbacks to rate limit, we can ignore this floor.
  if (now >= animation_floor_time_ || num_swapbuffers_complete_pending_ > 0) {
    TRACE_EVENT0("renderer", "RenderWidget::AnimateIfNeeded")
    animation_floor_time_ = now + animationInterval;
    // Set a timer to call us back after animationInterval before
    // running animation callbacks so that if a callback requests another
    // we’ll be sure to run it at the proper time.
    animation_timer_.Stop();
    animation_timer_.Start(FROM_HERE, animationInterval, this, &RenderWidget::AnimationCallback);
    animation_update_pending_ = false;
    if (is_accelerated_compositing_active_ && compositor_) {
      compositor_->Animate(base::TimeTicks::Now());
    } else {
      double frame_begin_time = (base::TimeTicks::Now() – base::TimeTicks()).InSecondsF();
      webwidget_->animate(frame_begin_time);
    }
    return;
  }
  TRACE_EVENT0("renderer", "EarlyOut_AnimatedTooRecently");
  if (!animation_timer_.IsRunning()) {
    // This code uses base::Time::Now() to calculate the floor and next fire
    // time because javascript’s Date object uses base::Time::Now().  The
    // message loop uses base::TimeTicks, which on windows can have a
    // different granularity than base::Time.
    // The upshot of all this is that this function might be called before
    // base::Time::Now() has advanced past the animation_floor_time_.  To
    // avoid exposing this delay to javascript, we keep posting delayed
    // tasks until base::Time::Now() has advanced far enough.
    base::TimeDelta delay = animation_floor_time_ – now;
    animation_timer_.Start(FROM_HERE, delay, this, &RenderWidget::AnimationCallback);
  }
}

特别说明:RenderWidget 是在 ./content/renderer/render_widget.cc
中(content::RenderWidget)而非在 ./core/rendering/RenderWidget.cpp
中。笔者最早读 RenderWidget.cpp 还因为其中没有任何关于 animation
的代码而困惑了很久。

看到这里其实 requestAnimationFrame 的实现原理就很明显了:

  • 注册回调函数
  • 浏览器更新时触发 animate
  • animate 会触发所有注册过的 callback

这里的工作机制可以理解为所有权的转移,把触发帧更新的时间所有权交给浏览器内核,与浏览器的更新保持同步。这样做既可以避免浏览器更新与动画帧更新的不同步,又可以给予浏览器足够大的优化空间。
在往上的调用入口就很多了,很多函数(RenderWidget::didInvalidateRect,RenderWidget::CompleteInit等)会触发动画检查,从而要求一次动画帧的更新。

这里一张图说明 requestAnimationFrame
的实现机制(来自官方):
88bf必发唯一官网 2

题图:https://unsplash.com/photos/PEfMW274zGM By Kai Oberhäuser

1 赞 1 收藏 1
评论

88bf必发唯一官网 3

前言

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


本文介绍

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

元素)和脚本实现。

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

它们有如下的特征:

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

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

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

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

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


名词说明

动画帧请求回调函数列表

每个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();

原因分析

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

1、执行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值;

2、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值。

3、执行下一个“采样所有动画”的任务

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

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

解决方案

有下面两个方案:

1、执行requestAnimationFrame之后再执行cancelAnimationFrame。

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

var id = null;

function a(time) {

console.log(“animation”);

id = window.requestAnimationFrame(a);

window.cancelAnimationFrame(id);

}

a();

2、在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。

注意事项

1、在处理模型
中我们已经看到,在遍历执行拷贝的动画帧请求回调函数列表中的回调函数之前,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();

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

例如下面的代码在执行“id1 = window.requestAnimationFrame(animate);”和“id2

window.requestAnimationFrame(animate);”时会将两个元组(handle分别为id1、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;


参考资料

Timing control for script-based
animations

Browsing
contexts

The Document
object

《HTML5 Canvas核心技术》

理解DOM

Page
Visibility

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

HOW BROWSERS WORK: BEHIND THE SCENES OF MODERN WEB
BROWSERS