打字機效果即爲文字逐個輸出,實際上就是Web動畫。html
在Web應用中,實現動畫效果的方法比較多,JavaScript 中能夠經過定時器 setTimeout 來實現,css3 可使用 transition 和 animation 來實現,html5 中的 canvas 也能夠實現。html5
除此以外,html5 還提供一個專門用於請求動畫的 API,即 requestAnimationFrame(rAF),顧名思義就是 「請求動畫幀」。css3
接下來,咱們一塊兒來看看 打字機效果 的幾種實現。爲了便於理解,我會盡可能使用簡潔的方式進行實現,有興趣的話,你也能夠把這些實現改造的更有逼格、更具藝術氣息一點,由於編程,原本就是一門藝術。npm
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>
setTimeout()方法的返回值是一個惟一的數值(ID),上面的代碼中,咱們也作了setTimeout()返回值的打印,那麼,這個數值有什麼用呢?canvas
若是你想要終止setTimeout()方法的執行,那就必須使用 clearTimeout()方法來終止,而使用這個方法的時候,系統必須知道你到底要終止的是哪個setTimeout()方法(由於你可能同時調用了好幾個 setTimeout()方法),這樣clearTimeout()方法就須要一個參數,這個參數就是setTimeout()方法的返回值(數值),用這個數值來惟一肯定結束哪個setTimeout()方法。瀏覽器
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 比起 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() })();
與setTimeout相比,requestAnimationFrame最大的優點是由系統來決定回調函數的執行時機。具體一點講,若是屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,若是刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟着系統的刷新步伐走。
它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。
除了以上三種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>
初始文字是所有在頁面上的,只是容器的寬度爲0,設置文字超出部分隱藏,而後不斷改變容器的寬度;
設置border-right,並在關鍵幀上改變 border-color 爲transparent,右邊框就像閃爍的光標了。
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>
使用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>
參考資料:Typed.js官網 | Typed.js GitHub地址
固然,打字機效果的實現方式,也不只僅侷限於上面所說的幾種方法,本文的目的,也不在於蒐羅全部打字機效果的實現,若是那樣將毫無心義,接下來,咱們將會對CSS3動畫和JS動畫進行一些比較,並對setTimeout、setInterval 和 requestAnimationFrame的一些細節進行總結。
關於CSS動畫和JS動畫,有一種說法是CSS動畫比JS流暢,其實這種流暢是有前提的。藉此機會,咱們對CSS3動畫和JS動畫進行一個簡單對比。
JS動畫控制能力強,能夠在動畫播放過程當中對動畫進行精細控制,如開始、暫停、終止、取消等;
JS動畫效果比CSS3動畫豐富,功能涵蓋面廣,好比能夠實現曲線運動、衝擊閃爍、視差滾動等CSS難以實現的效果;
JS動畫大多數狀況下沒有兼容性問題,而CSS3動畫有兼容性問題;
JS在瀏覽器的主線程中運行,而主線程中還有其它須要運行的JS腳本、樣式計算、佈局、繪製任務等,對其干擾可能致使線程出現阻塞,從而形成丟幀的狀況;
對於幀速表現很差的低版本瀏覽器,CSS3能夠作到天然降級,而JS則須要撰寫額外代碼;
JS動畫每每須要頻繁操做DOM的css屬性來實現視覺上的動畫效果,這個時候瀏覽器要不停地執行重繪和重排,這對於性能的消耗是很大的,尤爲是在分配給瀏覽器的內存沒那麼寬裕的移動端。
部分狀況下瀏覽器能夠對動畫進行優化(好比專門新建一個圖層用來跑動畫),爲何說部分狀況下呢,由於是有條件的:
在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動畫比較少或者不觸發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);
let timerId = setTimeout(function tick() {
console.log('tick', timerId);
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
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()建立的計時器。