JavaScript 工作原理之十三-CSS 和 JS 動畫底層原理及如何優化其效能

Troland發表於2019-02-16

原文請查閱這裡,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland

本系列持續更新中,Github 地址請查閱這裡

這是 JavaScript 工作原理的第十三章。

概述

正如你所知,動畫在建立令人歎服的網路應用中扮演著一個關鍵角色。由於使用者越來越注重使用者體驗,商戶開始意識到完美,令人愉悅的使用者體驗的重要性,結果網路應用變得越來越重並且擁有更多動態互動的功能。這就要求網路應用提供更加複雜的動畫來實現平滑的狀態過渡貫穿於使用者的使用過程當中。現在,這已經司空見慣。使用者變得越來越挑剔,他們潛意識期許可以獲得快速響應和良好互動的使用者介面。

然而,讓介面具有動畫效果不一定是件簡單的事情。動畫的時機,方面及採用何種動畫效果都是很模糊的概念。

JavaScript 和 CSS 動畫比較

JavaScript 和 CSS 是建立網頁動畫的兩條主要途徑。兩種不分好賴,看情況用吧。

CSS 動畫

使用 CSS 動畫是讓元素在螢幕上移動的最簡單方法。

我們將會以如何讓元素在 X 和 X 座標上移動元素 50 畫素作為小示例開始。通過持續 1 秒的 CSS 過渡來移動元素。

.box {
  -webkit-transform: translate(0, 0);
  -webkit-transition: -webkit-transform 1000ms;

  transform: translate(0, 0);
  transition: transform 1000ms;
}

.box.move {
  -webkit-transform: translate(50px, 50px);
  transform: translate(50px, 50px);
}

當為元素新增 move 類的時候,改變 transform 的值然後開發發生過渡效果。

除了過渡持續時間,還有 easing 引數,它主要負責動畫體驗。該引數會在之後詳細介紹。

如果通過以上的程式碼片段可以建立單獨的樣式類來操作動畫,那麼也可以使用 JavaScript 來切換每個動畫。

如下元素:

<div class="box">
  Sample content.
</div>

然後,使用 JavaScript 來切換每個動畫。

var boxElements = document.getElementsByClassName(`box`),
    boxElementsLength = boxElements.length,
    i;

for (i = 0; i < boxElementsLength; i++) {
  boxElements[i].classList.add(`move`);
}

以上程式碼片段為每個包含 box 類的元素新增 move 類來觸發動畫。

這樣做可以很好為你的網路應用提供很好的平衡。你就可以專注於使用 JavaScript 來操作應用狀態,然後只需為目標元素設定合適的類,讓瀏覽器來處理動畫。如若你選擇這麼處理,就可以監聽元素的 transitionend 事件,除了處理IE 老版本瀏覽器相容問題之外。

如下監聽 transitioned 事件,該事件會在動畫結束時觸發。

var boxElement = document.querySelector(`.box`); // 獲取第一個包含 box 類的元素
boxElement.addEventListener(`transitionend`, onTransitionEnd, false);

function onTransitionEnd() {
  // Handle the transition finishing.
}

除了使用 CSS 過渡,還可以使用 CSS 動畫,CSS 動畫可以讓你更好地控制單獨的動畫關鍵幀,持續時間以及迴圈次數。

關鍵幀是用來通知瀏覽器在規定的時間點上應有的 CSS 屬性值然後填充空白。

看下例子:

/**
 * 該示例是沒有包含瀏覽器字首的精簡版。加上以後會更加準確些。
 *
 */
.box {
  /* 選擇動畫名稱 */
  animation-name: movingBox;

  /* 動畫時長 */
  animation-duration: 2300ms;

  /* 動畫迴圈次數 */
  animation-iteration-count: infinite;

  /* 每次奇數次迴圈時反轉動畫 */
  animation-direction: alternate;
}

@keyframes movingBox {
  0% {
    transform: translate(0, 0);
    opacity: 0.4;
  }

  25% {
    opacity: 0.9;
  }

  50% {
    transform: translate(150px, 200px);
    opacity: 0.2;
  }

  100% {
    transform: translate(40px, 30px);
    opacity: 0.8;
  }
}

效果示例-https://sessionstack.github.i…

通過使用 CSS 動畫定義獨立於目標元素的動畫本身,然後設定元素的 animation-name 屬性來使用想要的動畫效果。

CSS 動畫仍然是需要加瀏覽器字首的,在 Safari, Safari 移動瀏覽器和 Android 端新增 -webkit- 字首。Chrome, Opera, Internet Explorer, and Firefox 端全部不需要新增字首。有很多工具可以用來建立包含任意字首的樣式,這樣就不需要在原始檔中帶樣式字首。

可以使用 autoprefixer 或者 cssnext 來自動為樣式新增字首。

JavaScript 動畫

和 CSS 過渡或者 CSS 動畫相比,使用 JavaScript 來建立動畫要更加複雜些,但是一般而言,它會為開發進行提供強大的功能。

一般情況下,可以內聯 JavaScript 動畫作為程式碼的一部分。也可以把它們封裝在其它物件之中。以下為復現之前描述的 CSS 過渡的 JavaScript 程式碼:

var boxElement = document.querySelector(`.box`);
var animation = boxElement.animate([
  {transform: `translate(0)`},
  {transform: `translate(150px, 200px)`}
], 500);
animation.addEventListener(`finish`, function() {
  boxElement.style.transform = `translate(150px, 200px)`;
});

預設情況下,網頁動畫只是修改了元素的展示效果。如果想要讓元素停留在其移動到的目標位置,那麼就得在動畫結束的時候修改其底層樣式。這也是為什麼在以上的示例中監聽 finish 事件然後設定 box.style.transform 屬性為 translate(150px, 200px) 的原因,該屬性值和 CSS 動畫執行的第二個樣式轉換是一樣的。

通過使用 JavaScript 動畫,可以完全控制每一步元素的樣式。這意味著可以隨心所欲地減速,暫停,停止或者翻轉動畫進而操作目標元素。由於可以適當地封裝動畫行為,所以當在構建複雜物件導向的應用程式的時候會特別有用。

Easing 定義

自然平滑地移動會讓網路應用擁有更好的使用者互動體驗。

自然條件下,沒有事物可以直線地從一個點運動到另一個點。現實生活中,在我們周圍的物理世界中物體在移動的時候會加速或減速,因為我們並不生活在真空狀態下且有不同的因素來影響事物的執行狀態。人類的大腦會期望感受這樣的移動,所以當為網路應用製作動畫的時候,利用此類知識會對自己會有好處。

這是你所應該理解的術語:

  • 『ease in』-開始移動緩慢而後加速
  • 『ease out』-開始移動迅速而後減速

可以合併兩個動畫,比如 『ease in out』。

Easing 可以使得動畫更加自然平滑。

Easing 關鍵字

可以為 CSS 過渡和動畫選擇任意的 easing 方法。不同的關鍵字會影響動畫的 easing。你也可以完全自定義 easing 方法。

以下為可以選擇用來控制 easing 的 CSS 關鍵字:

  • linear
  • ease-in
  • ease-out
  • ease-in-out

讓我們深入瞭解並檢視他們的效果。

Linear 動畫

不使用任何的 easing 方法的動畫即為 linear

以下為 linear 過渡效果的圖示:

值隨著時間流逝,值等比增加。使用 linear 動效,會讓動畫不自然。一般來說,避免使用 linear 動效。

使用如下程式碼實現一個簡單的線性動畫:

transition: transform 500ms linear;

Ease-out 動畫

正如前所述,和 linear 對比,easing out 讓動畫快速啟動,結束時會減速。以下為圖示:

總之,easing out 是最適合做介面體驗的,因為快速地啟動會給人以快速響應的動畫的感覺,而結束時讓人感覺很平滑這得歸功於不一致的移動速度。

打個比喻,比如那些跑車,首先啟動速度相當的快,這就給人以愉悅的感覺。這個就比較符合人類對於動畫的感知。

有很多的方法來實現 ease out 動畫效果,而最簡單的即為 CSS 中的 ease-out 關鍵字。

transition: transform 500ms ease-out;

Ease-in 動畫

和 ease-out 動畫相反-其啟動慢然後結束時變快。圖示如下:

和 ease-out 動畫比較,由於他們啟動緩慢給人以反應卡頓的感覺,所以 ease-in 讓人感覺動畫不自然。動畫結束時很快給人一種奇怪的感覺,因為整個動畫一直在加速,而現實世界中當事物突然停止運動的時候會減速而不是加速。

和 ease-out 和 linear 動畫類似,使用 CSS 關鍵字來實現 ease-in 動畫:

transition: transform 500ms ease-in;

Ease-in-out 動畫

該動畫為 ease-in 和 ease-out 的合集。圖示如下:

不要設定動畫持續時間過長,否則會給人一種介面不響應的感覺。

使用 ease-in-out CSS 關鍵字來實現 ease-in-out 動畫:

transition: transform 500ms ease-in-out;

自定義 easing

你可以自定義自己的 easing 曲線,這樣就更有效地控制專案中的動畫。

實際上, ease-inlinearease 關鍵字對映到預定義貝塞爾曲線 ,可以在 CSS transitions specificationWeb Animations specification 中查詢更多關於貝塞爾曲線的內容。

貝塞爾曲線

讓我們看一下貝塞爾曲線的執行原理。一條貝塞爾曲線包含四個點,或者準確地說是包含兩組數值。每一對數值內包含表示三次貝塞爾曲線控制點的 X 和 Y 座標。貝塞爾曲線的起點座標為 (0, 0) ,終點座標為 (1, 1)。可以設定兩組數值對。每個控制點的 X 軸值必須在 [0, 1] 之間,而 Y 軸值可以超過 [0, 1],雖然規範並沒有明確允許超過的數值。即使每個控制點的 X 和 Y 值的微小差異都會輸出完全不同的貝塞爾曲線。

檢視維基百科關於貝塞爾曲線的說明,通俗一點講即,現在所說的即三次貝塞爾曲線,該曲線由四個點組成,P0, P1, P2, P3 組成,那麼,P0 和 P1 組成一對,P2 和 P3 組成一對,P1 和 P2 即為控制點,P0 和 P3 即為起始和結束節點。如下圖所示:

看下兩張擁有相近但不同座標的控制結點的貝塞爾曲線圖。

如你所見,兩張圖有很大不同。第一個控制點向量差異為 (0.045, 0.183),而第二個控制點向量差異為 (-0.427, -0.054)。

第二條曲線的樣式為:

transition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);

第一組數值為起始控制點的 X 和 Y 座標而第二組數值為第二個控制點的 X 和 Y 座標。

效能優化

你得維持動畫幀數為 60 幀每秒,否則會影響到使用者體驗。

和世界上其它事物一樣,動畫會有效能開銷。一些屬性的動畫效能開銷相比其它屬性要小。比如,為元素的 widthheight 做動畫會更改其幾何結構並且可能會造成頁面上的其它元素移動或者大小的改變。這一過程被稱為佈局。之前的文章中有詳細介紹過佈局和渲染。

總之,應該儘量避免為會引起佈局和繪製的屬性做動畫。對於大多數現代瀏覽器而言,即把動畫侷限於 opacitytransform 屬性。

Will-change

可以使用 will-change 來通知瀏覽器將會更改某個元素的屬性。這會允許瀏覽器當更改某個元素屬性的時候,事先進行最恰當的優化。但不要濫用 will-change,因為這樣做會適得其反,使得瀏覽器浪費更多的資源,從而造成更多的效能問題。

為 transforms 和 opacity 新增 will-change 程式碼如下:

.box {
  will-change: transform, opacity;
}

該屬性在 Chrome, Firefox,Opera 得到很好的相容。

如何選擇 JavaScript 和 CSS 來執行動畫

這個問題是無解的。只需謹記以下原則:

  • 基於 CSS 的動畫和原生支援的網頁動畫一般都是由被稱為『合成執行緒』的執行緒來處理的。這不同於瀏覽器的主執行緒,主執行緒是用來執行計算樣式,佈局,繪製及 JavaScript 程式碼的。這即意味著如果瀏覽器在主執行緒上執行耗時的任務,不會中斷動畫的執行。
  • 很多時候,也可以由合成執行緒來處理 transformsopacity 屬性值的更改。
  • 如果有任何動畫觸發繪製,佈局或者同時觸發兩者,『主執行緒』將不得不來進行處理。事實是基於 CSS 和 JavaScript 的動畫和佈局或者繪製的效能開銷將很有可能會阻塞所有和 CSS 或者 JavaScript 執行相關的工作,從而使得渲染問題變得毫無意義。

正確使用動畫

良好的動畫為專案新增一層令人愉快和互動的使用者體驗。你可以隨意使用動畫,不管是寬度,除錯,定位,顏色或背景色,但必須注意潛在的效能瓶頸。糟糕的動畫選擇會影響使用者體驗,所以動畫必須是高效和適當的。儘可能減少使用動畫。只使用動畫來讓使用者體驗流暢自然而不是濫用。

使用動畫進行互動

不要因為只是為了用而去使用動畫。相反,有策略性地使用動畫來加強使用者互動體驗。避免使用不必要的動畫來打斷或者阻礙使用者的使用。

避免為效能開銷大的屬性做動畫

比糟糕的動畫使用更糟的是那些會引起頁面卡頓的動畫。這類動畫讓使用者感到懊喪和不快。

引用資源

疑問

可以看下網上的這篇介紹貝塞爾曲線的文章,那麼可以如何使用貝塞爾曲線來做出令人驚歎的動畫呢?

本系列持續更新中,Github 地址請查閱這裡

相關文章