從打字機效果的N種實現來看JS定時器和前端動畫

首先,什麼是打字機效果呢?一圖勝千言,諸君請看:
svTc4O.md.gifcss

打字機效果即爲文字逐個輸出,實際上就是Web動畫。html

在Web應用中,實現動畫效果的方法比較多,JavaScript 中能夠經過定時器 setTimeout 來實現,css3 可使用 transition 和 animation 來實現,html5 中的 canvas 也能夠實現。html5

除此以外,html5 還提供一個專門用於請求動畫的 API,即 requestAnimationFrame(rAF),顧名思義就是 「請求動畫幀」。css3

接下來,咱們一塊兒來看看 打字機效果 的幾種實現。爲了便於理解,我會盡可能使用簡潔的方式進行實現,有興趣的話,你也能夠把這些實現改造的更有逼格、更具藝術氣息一點,由於編程,原本就是一門藝術。npm

打字機效果的 N 種實現

實現一:setTimeout()

setTimeout版本的實現很簡單,只需把要展現的文本進行切割,使用定時器不斷向DOM元素裏追加文字便可,同時,使用::after僞元素在DOM元素後面產生光標閃爍的效果。代碼和效果圖以下:編程

<!-- 樣式 -->
<style type="text/css">
  /* 設置容器樣式 */
  #content {
    height: 400px;
    padding: 10px;
    font-size: 28px;
    border-radius: 20px;
    background-color: antiquewhite;
  }
  /* 產生光標閃爍的效果 */
  #content::after{
      content: '|';
      color:darkgray;
      animation: blink 1s infinite;
  }
  @keyframes blink{
      from{
          opacity: 0;
      }
      to{
          opacity: 1;
      }
  }
</style>

<body>
  <div id='content'></div>
  <script>
    (function () {
    // 獲取容器
    const container = document.getElementById('content')
    // 把須要展現的所有文字進行切割
    const data = '最簡單的打字機效果實現'.split('')
    // 須要追加到容器中的文字下標
    let index = 0
    function writing() {
      if (index < data.length) {
        // 追加文字
        container.innerHTML += data[index ++]
        let timer = setTimeout(writing, 200)
        console.log(timer) // 這裏會依次打印 1 2 3 4 5 6 7 8 9 10
      }
    }
    writing()
  })();
</script>
</body>

svTWgH.md.gif
setTimeout()方法的返回值是一個惟一的數值(ID),上面的代碼中,咱們也作了setTimeout()返回值的打印,那麼,這個數值有什麼用呢?canvas

若是你想要終止setTimeout()方法的執行,那就必須使用 clearTimeout()方法來終止,而使用這個方法的時候,系統必須知道你到底要終止的是哪個setTimeout()方法(由於你可能同時調用了好幾個 setTimeout()方法),這樣clearTimeout()方法就須要一個參數,這個參數就是setTimeout()方法的返回值(數值),用這個數值來惟一肯定結束哪個setTimeout()方法。瀏覽器

實現二:setInterval()

setInterval實現的打字機效果,其實在MDN window.setInterval 案例三中已經有一個了,並且還實現了播放、暫停以及終止的控制,效果可點擊這裏查看,在此只進行setInterval打字機效果的一個最簡單實現,其實代碼和前文setTimeout的實現相似,效果也一致。bash

(function () {
  // 獲取容器
  const container = document.getElementById('content')
  // 把須要展現的所有文字進行切割
  const data = '最簡單的打字機效果實現'.split('')
  // 須要追加到容器中的文字下標
  let index = 0
  let timer = null
  function writing() {
    if (index < data.length) {
      // 追加文字
      container.innerHTML += data[index ++]
      // 沒錯,也能夠經過,clearTimeout取消setInterval的執行
      // index === 4 && clearTimeout(timer)
    } else {
      clearInterval(timer)
    }
    console.log(timer) // 這裏會打印出 1 1 1 1 1 ...
  }
  // 使用 setInterval 時,結束後不要忘記進行 clearInterval
  timer = setInterval(writing, 200)
})();

和setTimeout同樣,setInterval也會返回一個 ID(數字),能夠將這個ID傳遞給clearInterval()或者clearTimeout() 以取消定時器的執行。css3動畫

在此有必要強調一點:定時器指定的時間間隔,表示的是什麼時候將定時器的代碼添加到消息隊列,而不是什麼時候執行代碼。因此真正什麼時候執行代碼的時間是不能保證的,取決於什麼時候被主線程的事件循環取到,並執行。

實現三:requestAnimationFrame()

在動畫的實現上,requestAnimationFrame 比起 setTimeout 和 setInterval來無疑更具優點。咱們先看看打字機效果的requestAnimationFrame實現:

(function () {
    const container = document.getElementById('content')
    const data = '與 setTimeout 相比,requestAnimationFrame 最大的優點是 由系統來決定回調函數的執行時機。具體一點講就是,系統每次繪製以前會主動調用 requestAnimationFrame 中的回調函數,若是系統繪製率是 60Hz,那麼回調函數就每16.7ms 被執行一次,若是繪製頻率是75Hz,那麼這個間隔時間就變成了 1000/75=13.3ms。換句話說就是,requestAnimationFrame 的執行步伐跟着系統的繪製頻率走。它能保證回調函數在屏幕每一次的繪製間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。'.split('')
    let index = 0
    function writing() {
      if (index < data.length) {
        container.innerHTML += data[index ++]
        requestAnimationFrame(writing)
      }
    }
    writing()
  })();

svT5DI.md.gif
與setTimeout相比,requestAnimationFrame最大的優點是由系統來決定回調函數的執行時機。具體一點講,若是屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,若是刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟着系統的刷新步伐走。

它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。

實現四:CSS3

除了以上三種js方法以外,其實只用CSS咱們也能夠實現打字機效果。大概思路是藉助CSS3的@keyframes來不斷改變包含文字的容器的寬度,超出容器部分的文字隱藏不展現。

<style>
  div {
    font-size: 20px;
    /* 初始寬度爲0 */
    width: 0;
    height: 30px;
    border-right: 1px solid darkgray;
    /*
    Steps(<number_of_steps>,<direction>)
    steps接收兩個參數:第一個參數指定動畫分割的段數;第二個參數可選,接受 start和 end兩個值,指定在每一個間隔的起點或是終點發生階躍變化,默認爲 end。
    */
    animation: write 4s steps(14) forwards,
      blink 0.5s steps(1) infinite;
      overflow: hidden;
  }

  @keyframes write {
    0% {
      width: 0;
    }

    100% {
      width: 280px;
    }
  }

  @keyframes blink {
    50% {
      /* transparent是全透明黑色(black)的速記法,即一個相似rgba(0,0,0,0)這樣的值。*/
      border-color: transparent; /* #00000000 */
    }
  }
</style>

<body>
  <div>
    大江東去浪淘盡,千古風流人物
  </div>
</body>

svTL8g.md.gif
以上CSS打字機效果的原理一目瞭然:

初始文字是所有在頁面上的,只是容器的寬度爲0,設置文字超出部分隱藏,而後不斷改變容器的寬度;

設置border-right,並在關鍵幀上改變 border-color 爲transparent,右邊框就像閃爍的光標了。

實現五:Typed.js

Typed.js is a library that types. Enter in any string, and watch it type at the speed you've set, backspace what it's typed, and begin a new sentence for however many strings you've set.

Typed.js是一個輕量級的打字動畫庫, 只須要幾行代碼,就能夠在項目中實現炫酷的打字機效果(本文第一張動圖即爲Typed.js實現)。源碼也相對比較簡單,有興趣的話,能夠到GitHub進行研讀。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/typed.js@2.0.11"></script>
</head>

<body>
  <div id="typed-strings">
    <p>Typed.js is a <strong>JavaScript</strong> library.</p>
    <p>It <em>types</em> out sentences.</p>
  </div>
  <span id="typed"></span>
</body>
<script>
  var typed = new Typed('#typed', {
    stringsElement: '#typed-strings',
    typeSpeed: 60
  });
</script>

</html>

svTxrn.md.gif
使用Typed.js,咱們也能夠很容易的實現對動畫開始、暫停等的控制:

<body>
  <input type="text" class="content" name="" style="width: 80%;">
  <br>
  <br>
  <button class="start">開始</button>
  <button class="stop">暫停</button>
  <button class="toggle">切換</button>
  <button class="reset">重置</button>
</body>
<script>const startBtn = document.querySelector('.start');
const stopBtn = document.querySelector('.stop');
const toggleBtn = document.querySelector('.toggle');
const resetBtn = document.querySelector('.reset');
const typed = new Typed('.content',{
  strings: ['雨過白鷺州,留戀銅雀樓,斜陽染幽草,幾度飛紅,搖曳了江上遠帆,回望燈如花,未語人先羞。'],
  typeSpeed: 200,
  startDelay: 100,
  loop: true,
  loopCount: Infinity,
  bindInputFocusEvents:true
});
startBtn.onclick = function () {
  typed.start();
}
stopBtn.onclick = function () {
  typed.stop();
}
toggleBtn.onclick = function () {
  typed.toggle();
}
resetBtn.onclick = function () {
  typed.reset();
}
</script>

sv7pV0.md.gif
參考資料:Typed.js官網 | Typed.js GitHub地址
固然,打字機效果的實現方式,也不只僅侷限於上面所說的幾種方法,本文的目的,也不在於蒐羅全部打字機效果的實現,若是那樣將毫無心義,接下來,咱們將會對CSS3動畫和JS動畫進行一些比較,並對setTimeout、setInterval 和 requestAnimationFrame的一些細節進行總結。

CSS3動畫和JS動畫的比較

關於CSS動畫和JS動畫,有一種說法是CSS動畫比JS流暢,其實這種流暢是有前提的。藉此機會,咱們對CSS3動畫和JS動畫進行一個簡單對比。

JS動畫

  • 優勢:

JS動畫控制能力強,能夠在動畫播放過程當中對動畫進行精細控制,如開始、暫停、終止、取消等;

JS動畫效果比CSS3動畫豐富,功能涵蓋面廣,好比能夠實現曲線運動、衝擊閃爍、視差滾動等CSS難以實現的效果;

JS動畫大多數狀況下沒有兼容性問題,而CSS3動畫有兼容性問題;

  • 缺點:

JS在瀏覽器的主線程中運行,而主線程中還有其它須要運行的JS腳本、樣式計算、佈局、繪製任務等,對其干擾可能致使線程出現阻塞,從而形成丟幀的狀況;

對於幀速表現很差的低版本瀏覽器,CSS3能夠作到天然降級,而JS則須要撰寫額外代碼;

JS動畫每每須要頻繁操做DOM的css屬性來實現視覺上的動畫效果,這個時候瀏覽器要不停地執行重繪和重排,這對於性能的消耗是很大的,尤爲是在分配給瀏覽器的內存沒那麼寬裕的移動端。

CSS3動畫

  • 優勢:

部分狀況下瀏覽器能夠對動畫進行優化(好比專門新建一個圖層用來跑動畫),爲何說部分狀況下呢,由於是有條件的:

在Chromium基礎上的瀏覽器中

同時CSS動畫不觸發layout或paint,在CSS動畫或JS動畫觸發了paint或layout時,須要main thread進行Layer樹的重計算,這時CSS動畫或JS動畫都會阻塞後續操做。

部分效果能夠強制使用硬件加速 (經過 GPU 來提升動畫性能)

  • 缺點:

代碼冗長。CSS 實現稍微複雜一點動畫,CSS代碼可能都會變得很是笨重;

運行過程控制較弱。css3動畫只能在某些場景下控制動畫的暫停與繼續,不能在特定的位置添加回調函數。

main thread(主線程)和compositor thread(合成器線程)

渲染線程分爲main thread(主線程)和compositor thread(合成器線程)。主線程中維護了一棵Layer樹(LayerTreeHost),管理了TiledLayer,在compositor thread,維護了一樣一顆LayerTreeHostImpl,管理了LayerImpl,這兩棵樹的內容是拷貝關係。所以能夠彼此不干擾,當Javascript在main thread操做LayerTreeHost的同時,compositor thread能夠用LayerTreeHostImpl作渲染。當Javascript繁忙致使主線程卡住時,合成到屏幕的過程也是流暢的。

爲了實現防假死,鼠標鍵盤消息會被首先分發到compositor thread,而後再到main thread。這樣,當main thread繁忙時,compositor thread仍是可以響應一部分消息,例如,鼠標滾動時,若是main thread繁忙,compositor thread也會處理滾動消息,滾動已經被提交的頁面部分(未被提交的部分將被刷白)。

CSS動畫比JS動畫流暢的前提

CSS動畫比較少或者不觸發pain和layout,即重繪和重排時。例如經過改變以下屬性生成的css動畫,這時整個CSS動畫得以在compositor thread完成(而JS動畫則會在main thread執行,而後觸發compositor進行下一步操做):

backface-visibility:該屬性指定當元素背面朝向觀察者時是否可見(3D,實驗中的功能);

opacity:設置 div 元素的不透明級別;

perspective 設置元素視圖,該屬性隻影響 3D 轉換元素;

perspective-origin:該屬性容許您改變 3D 元素的底部位置;

transform:該屬性應用於元素的2D或3D轉換。這個屬性容許你將元素旋轉,縮放,移動,傾斜等。

JS在執行一些昂貴的任務時,main thread繁忙,CSS動畫因爲使用了compositor thread能夠保持流暢;

部分屬性可以啓動3D加速和GPU硬件加速,例如使用transform的translateZ進行3D變換時;

經過設置 will-change 屬性,瀏覽器就能夠提早知道哪些元素的屬性將會改變,提早作好準備。待須要改變元素的時機到來時,就能夠馬上實現它們,從而避免卡頓等問題。

不要將 will-change 應用到太多元素上,若是過分使用的話,可能致使頁面響應緩慢或者消耗很是多的資源。

例以下面的代碼就是提早告訴渲染引擎 box 元素將要作幾何變換和透明度變換操做,這時候渲染引擎會將該元素單獨實現一幀,等這些變換髮生時,渲染引擎會經過合成線程直接去處理變換,這些變換並無涉及到主線程,這樣就大大提高了渲染的效率。

.box {will-change: transform, opacity;}
setTimeout、setInterval 和 requestAnimationFrame 的一些細節

setTimeout 和 setInterval

setTimeout 的執行時間並非肯定的。在JavaScript中,setTimeout 任務被放進了異步隊列中,只有當主線程上的任務執行完之後,纔會去檢查該隊列裏的任務是否須要開始執行,因此 setTimeout 的實際執行時機通常要比其設定的時間晚一些。

刷新頻率受 屏幕分辨率 和 屏幕尺寸 的影響,不一樣設備的屏幕繪製頻率可能會不一樣,而 setTimeout 只能設置一個固定的時間間隔,這個時間不必定和屏幕的刷新時間相同。

setTimeout 的執行只是在內存中對元素屬性進行改變,這個變化必需要等到屏幕下次繪製時纔會被更新到屏幕上。若是二者的步調不一致,就可能會致使中間某一幀的操做被跨越過去,而直接更新下一幀的元素。假設屏幕每隔16.7ms刷新一次,而setTimeout 每隔10ms設置圖像向左移動1px, 就會出現以下繪製過程:

第 0 ms:屏幕未繪製,等待中,setTimeout 也未執行,等待中;

第 10 ms:屏幕未繪製,等待中,setTimeout 開始執行並設置元素屬性 left=1px;

第 16.7 ms:屏幕開始繪製,屏幕上的元素向左移動了 1px, setTimeout 未執行,繼續等待中;

第 20 ms:屏幕未繪製,等待中,setTimeout 開始執行並設置 left=2px;

第 30 ms:屏幕未繪製,等待中,setTimeout 開始執行並設置 left=3px;

第 33.4 ms:屏幕開始繪製,屏幕上的元素向左移動了 3px, setTimeout 未執行,繼續等待中;

...

從上面的繪製過程當中能夠看出,屏幕沒有更新 left=2px 的那一幀畫面,元素直接從left=1px 的位置跳到了 left=3px 的的位置,這就是丟幀現象,這種現象就會引發動畫卡頓。

setInterval的回調函數調用之間的實際延遲小於代碼中設置的延遲,由於回調函數執行所需的時間「消耗」了間隔的一部分,若是回調函數執行時間長、執行次數多的話,偏差也會愈來愈大:

// repeat with the interval of 2 seconds
let timerId = setInterval(() => console.log('tick', timerId), 2000);
// after 50 seconds stop
setTimeout(() => {
  clearInterval(timerId);
  console.log('stop', timerId);
}, 50000);

sv7eq1.md.png
嵌套的setTimeout能夠保證固定的延遲:

let timerId = setTimeout(function tick() {
console.log('tick', timerId);
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
sv7nVx.md.png

requestAnimationFrame
除了上文提到的requestAnimationFrame的優點外,requestAnimationFrame還有如下兩個優點:

CPU節能:使用setTimeout實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後臺執行動畫任務,因爲此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,徹底是浪費CPU資源。

而requestAnimationFrame則徹底不一樣,當頁面處於未激活的狀態下,該頁面的屏幕刷新任務也會被系統暫停,所以跟着系統步伐走的requestAnimationFrame也會中止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了CPU開銷。

函數節流:在高頻率事件(resize,scroll等)中,爲了防止在一個刷新間隔內發生屢次函數執行,使用requestAnimationFrame可保證每一個刷新間隔內,函數只被執行一次,這樣既能保證流暢性,也能更好的節省函數執行的開銷。

一個刷新間隔內函數執行屢次是沒有意義的,由於顯示器每16.7ms刷新一次,屢次繪製並不會在屏幕上體現出來。

  • 關於最小時間間隔

2011年的標準中是這麼規定的:

setTimeout:若是當前正在運行的任務是由setTimeout()方法建立的任務,而且時間間隔小於4ms,則將時間間隔增長到4ms;

setInterval:若是時間間隔小於10ms,則將時間間隔增長到10ms。

在最新標準中:若是時間間隔小於0,則將時間間隔設置爲0。若是嵌套級別大於5,而且時間間隔小於4ms,則將時間間隔設置爲4ms。

  • 定時器的清除

因爲clearTimeout()和clearInterval()清除的是同一列表(活動計時器列表)中的條目,所以可使用這兩種方法清除setTimeout()或 setInterval()建立的計時器。

相關文章
相關標籤/搜索