浅析 requestAnimationFrame
2017/03/02 · JavaScript · 1 评论 · requestAnimationFrame
原作出处: 天猫商城前端团队(FED卡塔 尔(英语:State of Qatar)- 腾渊
深信以后一大半人在 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卡塔 尔(英语:State of Qatar),不过对于浏览器内核对渲染函数的响应以致几时能够发起下二个动漫帧的机会,是全然不了然的。对于浏览器内核来说,它能够驾驭发起下一个渲染帧的确切时机,然则对于其余setTimeout 和 setInterval 传入的回调函数实行,都以同等对待的,它很难领悟哪些回调函数是用于动漫渲染的,由此,优化的时机特别难以领悟。谬论就在于,写 JavaScript 的人领悟生龙活虎帧卡通在哪行代码开头,哪行代码甘休,却不精通应该哪天先导,应该几时截至,而在根本引擎来讲,事情却适逢其时相反,所以两个很难完美合营,直到 requestAnimationFrame 出现。
自家很欢乐 requestAnimationFrame 那几个名字,因为起得不行直白 – request animation frame,对于那几个 API 最佳的说明正是名字自己了。那样一个API,你传入的 API 不是用来渲染生龙活虎帧动漫,你上街都不好意思跟人公告。
出于自己是个喜欢读书代码的人,为了展现团结好学的势态,特意读了下 Chrome 的代码去询问它是怎么落实 requestAnimationFrame 的(代码基于 Android 4.4卡塔 尔(英语:State of Qatar):
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
的兑现机制(来自官方):
题图: By Kai Oberhäuser
1 赞 1 收藏 1 评论
名词表明
比方下边包车型客车代码在实践“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
前言
正文首要参照w3c资料,从底层达成原理的角度介绍了requestAnimationFrame、cancelAnimationFrame,给出了相关的亲自去做代码以至自己对贯彻原理的知晓和座谈。
正文介绍
浏览器中卡通有三种实现方式:通过申明成分实现(如SVG中的
要素卡塔尔和本子完毕。
能够经过setTimeout和setInterval方法来在剧本中得以达成动漫,不过如此效果说不定缺乏流畅,且会占领额外的能源。可参照《Html5 Canvas大旨技术》中的论述:
它们犹如下的风味:
1、即便向其传递微秒为单位的参数,它们也无法达到ms的准头。这是因为javascript是单线程的,大概会发出围堵。
2、未有对调用动画的循环机制举行优化。
3、未有设想到绘制动画的最棒机会,只是始终地以有些大概的事件间距来调用循环。
实在,使用setInterval或setTimeout来兑现主循环,根本错误就在于它们抽象品级不切合要求。大家想让浏览器实行的是大器晚成套能够操纵各个细节的api,完成如“最优帧速率”、“采取绘制下黄金年代帧的最棒机遇”等职能。可是只要选取它们来讲,那么些现实的底细就务须由开垦者自身来完结。
requestAnimationFrame不供给使用者钦赐循环间隔时间,浏览器会基于当前页面是或不是可知、CPU的负载情况等源于行决定最好的帧速率,进而更客观地采取CPU。
cancelAnimationFrame
cancelAnimationFrame 方法用于撤除在此以前布局的一个动漫帧更新的呼吁。
当调用cancelAnimationFrame(handle)时,浏览器会设置该handle指向的回调函数的cancelled为true。
任由该回调函数是或不是在动漫帧恳求回调函数列表中,它的cancelled都会被设置为true。
要是该handle没有指向任何回调函数,则调用cancelAnimationFrame 不会时有产生任何业务。
本文由澳门在线威尼斯官方发布于威尼斯澳门在线,转载请注明出处:深入理解requestAnimationFrame
关键词: