澳门新葡亰平台官网:码农终结者

浅析 requestAnimationFrame

澳门新葡亰平台官网 ,2017/03/02 · JavaScript
· 1 评论 ·
requestAnimationFrame

原稿出处: 天猫商城前端团队(FED)-
腾渊   

澳门新葡亰平台官网 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
的兑现机制(来自官方):
澳门新葡亰平台官网 2

题图: By Kai Oberhäuser

1 赞 1 收藏 1
评论

澳门新葡亰平台官网 3

例如说下面包车型大巴代码在进行“id1 = window.requestAnimationFrame(animate卡塔尔;”和“id2

window.requestAnimationFrame(animate卡塔尔(英语:State of Qatar);”时会将三个元组(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卡塔尔(英语:State of Qatar); 
//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

cancelAnimationFrame

cancelAnimationFrame 方法用于裁撤早先布置的八个动漫帧更新的须求。

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

无论是该回调函数是或不是在动漫帧哀告回调函数列表中,它的cancelled都会被安装为true。

譬喻该handle未有照准任何回调函数,则调用cancelAnimationFrame
不会生出其余事情。

requestAnimationFrame

requestAnimationFrame方法用于公告浏览器重采集样本动漫。

当requestAnimationFrame(callback卡塔尔国被调用时不会施行callback,而是会将元组<
handle,callback>插入到动漫帧诉求回调函数列表末尾(其兰月组的callback正是流传requestAnimationFrame的回调函数),况兼再次回到handle值,该值为浏览器定义的、大于0的整数,唯风流倜傥标志了该回调函数在列表中地方。

每种回调函数都有贰个布尔标记cancelled,该标志开端值为false,并且对外不可以知道。

在后面包车型大巴“管理模型”
中大家会看见,浏览器在执行“采集样板全数动漫”的职分时会遍历动画帧央浼回调函数列表,决断每种元组的callback的cancelled,假使为false,则试行callback。

管理模型

当页面可以见到并且动漫帧伏乞回调函数列表不为空时,浏览器会定时地投入叁个“采集样本全数动漫”的任务到UI线程的行列中。

这里使用伪代码来声明“采样全数动漫”职责的实施步骤:

var list = {};

var browsingContexts = 浏览器超级上下文及其属下的浏览器上下文;

for (var browsingContext in browsingContexts) {

var time = 从1969年5月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) {

}

}

}

}

名词表明

发表评论

电子邮件地址不会被公开。 必填项已用*标注