相信現在絕大多數人在 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):
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 裡面做了些什麼:
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):
1 2 3 4 5 6 7 |
void PageWidgetDelegate::animate(Page* page, double monotonicFrameBeginTime) { FrameView* view = mainFrameView(page); if (!view) return; view->serviceScriptedAnimations(monotonicFrameBeginTime); } |
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(); } } |
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 的實現機制(來自官方):
題圖:https://unsplash.com/photos/PEfMW274zGM By Kai Oberhäuser