一、前言
前端實現動畫效果主要有以下幾種方法:CSS3中的transition 和 animation ,Javascript 中可以通過定時器 setTimeout、setinterval,HTML5 canvas,HTML5提供的requestAnimationFrame。本文主要分析setTimeout、setinterval、requestAnimationFrame三者的區別和他們各自的優缺點。在瞭解他們三個之前,我們先來看看一些相關概念。
二、相關概念介紹
1.螢幕重新整理頻率
即影像在螢幕上更新的速度,也即螢幕上的影像每秒鐘出現的次數,它的單位是赫茲(Hz)。 對於一般膝上型電腦,這個頻率大概是60Hz。這個值的設定受螢幕解析度、螢幕尺寸和顯示卡的影響。
2.動畫原理
動畫本質就是要讓人眼看到影像被重新整理而引起變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。在螢幕每次重新整理前,將影像的位置向左移動一個畫素,即1px。螢幕每次刷出來的影像位置都比前一個要差1px,你就會看到影像在移動;由於我們人眼的視覺停留效應,當前位置的影像停留在大腦的印象還沒消失,緊接著影像又被移到了下一個位置,因此你才會看到影像在流暢的移動,這就是視覺效果上形成的動畫。
三、setInterval
1.執行機制
按照指定的週期(以毫秒計)來呼叫函式或計算表示式。方法會不停地呼叫函式(當頁面被隱藏或者最小化時,setInterval()
仍在後臺繼續執行,這種動畫重新整理是完全沒有意義的,對cpu也是極大的浪費),直到 clearInterval() 被呼叫或視窗被關閉。
setinterval的執行時間不確定,引數中的時間間隔是將程式碼新增到非同步佇列中等待的時間。只有當主執行緒中的任務以及佇列前面的任務是執行完畢,才真正開始執行動畫程式碼。
注:HTML5標準規定,setInterval的最短間隔時間是10毫秒,也就是說,小於10毫秒的時間間隔會被調整到10毫秒。
2.語法
setinterval(code, milliseconds);
setinterval(function, milliseconds, param1, param2, ...)
引數 | 描述 |
---|---|
code/function | 必需。要呼叫一個程式碼串,也可以是一個函式。 |
milliseconds | 必須。週期性執行或呼叫 code/function 之間的時間間隔,以毫秒計。 |
param1, param2, ... | 可選。 傳給執行函式的其他引數(IE9 及其更早版本不支援該引數)。 |
3.例項
//每三秒(3000 毫秒)彈出 "Hello":
var myVar;
function myFunction() {
myVar = setInterval(alertFunc, 3000);
}
function alertFunc() {
alert("Hello!");
}
4.清除setInterval
clearinterval() 方法可取消由 setinterval() 函式設定的定時執行操作。引數必須是由 setinterval() 返回的 id 值。 注意: 要使用 clearinterval() 方法, 在建立執行定時操作時要使用全域性變數.清除示例如下:
var myVar = setInterval(function(){ setColor() }, 300);
function setColor() {
var x = document.body;
x.style.backgroundColor = x.style.backgroundColor == "yellow" ? "pink" : "yellow";
}
function stopColor() {
clearInterval(myVar);
}
5.缺點
(1)setinterval()無視程式碼錯誤,如果setinterval執行的程式碼由於某種原因出了錯,它還會持續不斷地呼叫該程式碼。
(2)setinterval無視網路延遲,由於某些原因(伺服器過載、臨時斷網、流量劇增、使用者頻寬受限,等等),你的請求要花的時間遠比你想象的要長。但setinterval不在乎。它仍然會按定時持續不斷地觸發請求,最終你的客戶端網路佇列會塞滿呼叫函式。
(3) setinterval不保證執行,與settimeout不同,並不能保證到了時間間隔,程式碼就準能執行。如果你呼叫的函式需要花很長時間才能完成,那某些呼叫會被直接忽略
四、setTimeout
1.執行機制
在指定的毫秒數後呼叫函式或計算表示式。每次函式執行的時候都會建立換一個新的定時器。在前一個定時器程式碼執行完之前,不會向佇列插入新的定時器程式碼,確保不會有任何確實的間隔。並且確保在下一次定時器程式碼執行之前,至少要等待指定的間隔,避免了連續的執行。當方法執行完成定時器就立即停止(但是定時器還在,只不過沒用了);
2.語法(同setInterval)
3.例項
//3 秒(3000 毫秒)後彈出 "Hello" :
var myVar;
function myFunction() {
myVar = setTimeout(alertFunc, 3000);
}
function alertFunc() {
alert("Hello!");
}
4.清除setTimeout
使用cleartimeout函式,用法同clearinterval
5.缺點
(1)利用seTimeout實現的動畫在某些低端機上會出現卡頓、抖動的現象。
(2)settimeout的執行時間並不是確定的。在javascript中, settimeout 任務被放進了非同步佇列中,只有當主執行緒上的任務執行完以後,才會去檢查該佇列裡的任務是否需要開始執行,因此 settimeout 的實際執行時間一般要比其設定的時間晚一些。
(3)重新整理頻率受螢幕解析度和螢幕尺寸的影響,因此不同裝置的螢幕重新整理頻率可能會不同,而 settimeout只能設定一個固定的時間間隔,這個時間不一定和螢幕的重新整理時間相同。
(4)settimeout的執行只是在記憶體中對影像屬性進行改變,這個變化必須要等到螢幕下次重新整理時才會被更新到螢幕上。如果兩者的步調不一致,就可能會導致中間某一幀的操作被跨越過去,而直接更新下一幀的影像。
五、requestAnimationFrame(推薦使用)
1.執行機制
告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前呼叫指定的回撥函式更新動畫。不需要設定時間間隔,是由系統的時間間隔定義的。大多數瀏覽器的重新整理頻率是60Hz(每秒鐘反覆繪製60次),迴圈間隔是1000/60,約等於16.7ms。不需要呼叫者指定幀速率,瀏覽器會自行決定最佳的幀效率。只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。
2.語法
window.requestanimationframe(callback);
引數callback:下一次重繪之前更新動畫幀所呼叫的函式(即上面所說的回撥函式)。
3.例項
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
4.缺點
requestanimationframe 不管理回撥函式,即在回撥被執行前,多次呼叫帶有同一回撥函式的 requestanimationframe,會導致回撥在同一幀中執行多次。我們可以通過一個簡單的例子模擬在同一幀內多次呼叫 requestanimationframe 的場景:(mousemove, scroll 這類事件常見)
const animation = timestamp => console.log('animation called at', timestamp)
window.requestAnimationFrame(animation)
window.requestAnimationFrame(animation)
// animation called at 320.7559999991645
// animation called at 320.7559999991645
我們用連續呼叫兩次 requestanimationframe 模擬在同一幀中呼叫兩次 requestanimationframe。 例子中的 timestamp 是由 requestanimationframe 傳給回撥函式的,表示回撥佇列被觸發的時間。由輸出可知,animation 函式在同一幀內被執行了兩次,即繪製了兩次動畫。
ps:解決辦法
對於這種高頻發事件,一般的解決方法是使用節流函式。但是在這裡使用節流函式並不能完美解決問題。因為節流函式是通過時間管理佇列的,而 requestanimationframe 的觸發時間是不固定的,在高重新整理頻率的螢幕上時間會小於 16.67ms,頁面如果被推入後臺,時間可能大於 16.67ms。
完美的解決方案是通過 requestanimationframe 來管理佇列,其思路就是保證 requestanimationframe 的佇列裡,同樣的回撥函式只有一個。示例程式碼如下:
const onScroll = e => {
if (scheduledAnimationFrame) { return }
scheduledAnimationFrame = true
window.requestAnimationFrame(timestamp => {
scheduledAnimationFrame = false
animation(timestamp)
})
}
window.addEventListener('scroll', onScroll)
5.與setTimeout和setInterval的區別
(1)requestanimationframe會把每一幀中的所有dom操作集中起來,在一次重繪或迴流中就完成,並且重繪或迴流的時間間隔緊緊跟隨瀏覽器的重新整理頻率
(2)在隱藏或不可見的元素中,requestanimationframe將不會進行重繪或迴流,這當然就意味著更少的cpu、gpu和記憶體使用量
(3)requestanimationframe是由瀏覽器專門為動畫提供的api,在執行時瀏覽器會自動優化方法的呼叫,並且如果頁面不是啟用狀態下的話,動畫會自動暫停,有效節省了cpu開銷
6.相容性封裝
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = (window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
var self = this, start, finish;
return window.setTimeout(function() {
start = +new Date();
callback(start);
finish = +new Date();
self.timeout = 1000/60 - (finish - start);
}, self.timeout);
});
}
程式碼解析:
這段程式碼先檢查了 window.requestanimationframe 函式的定義是否存在。如果不存在,就遍歷已知的各種瀏覽器實現並替代該函式。如果還是找不到一個與瀏覽器相關的實現,它最終會採用基於javascript定時器的動畫以每秒60幀的間隔呼叫settimeout函式。
mozrequestanimationframe() 會接收一個時間碼(從1970年1月1日起至今的毫秒數),表示下一次重繪的實際發生時間。這樣, mozrequestanimationframe() 就會根據這個時間碼設定將來的某個時刻進行重繪。
但是 webkitrequestanimationframe() 和 msrequestanimationframe() 不會給回撥函式傳遞時間碼,因此無法知道下一次重繪將發生在什麼時間。 如果要計算兩次重繪的時間間隔,firefox中可以使用既有的時間碼,而在chrome和ie則可以使用不太精確地date()物件。
7.清除動畫
cancelAnimationFrame(動畫名) ,類似clearTimeout函式
六、總結
1.執行次數:setInterval執行多次,setTimeout、requestAnimationframe執行一次
2.效能:setTimeout會出現丟幀、卡頓現象,setInterval會出現呼叫丟失情況,requestAnimationframe不會出現這些問題,頁面未啟用時不會執行動畫,減少了大量cpu消耗
3.相容性問題:setInterval,setTimeout在IE瀏覽器中不支援引數傳遞,能夠在大多數瀏覽器中正常使用。而requestAnimationframe不相容IE10以下
七、面試題
1.setTimeout中的this指向問題
var i = 0;
const o = {
i: 1;
fn: function(){
console.log(this.i);
}
}
setTimeout(o.fn, 1000); //執行後會列印出什麼
錯誤思路:setTimeout執行,呼叫物件O的fn函式,由於呼叫者是物件O,那麼this也指向了物件O,又物件O中有屬性i,則會列印出1。
正解:因為setTimeout是window物件的方法,傳入o.fn只是將o.fn這個函式傳給了setTimeout,仍然是window物件在呼叫。上面程式碼執行的正確結果是0,是因為定義了全域性變數i為0。如果沒有定義,則會輸出undefined。
ps:如果這裡不是setTimeout執行這個函式,而是o.fn(),那麼會輸出1。
2.執行下面的程式碼,控制檯如何輸出
(function () {
setTimeout(function () {
alert(2);
}, 0);
alert(1);
})()
先彈出的應該是1,而不是你以為“立即執行”的2。 settimeout,setinterval都存在一個最小延遲的問題,雖然你給的delay值為0,但是瀏覽器執行的是自己的最小值。html5標準是4ms,但並不意味著所有瀏覽器都會遵循這個標準,包括手機瀏覽器在內,這個最小值既有可能小於4ms也有可能大於4ms。在標準中,如果在settimeout中巢狀一個settimeout, 那麼巢狀的settimeout的最小延遲為10ms。
3.執行下面的程式碼,控制檯輸出什麼
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
輸出結果大家都只是會是5個6,由於JavaScript是單執行緒的,按順序執行,setTimeout是非同步函式,它會將 timer
函式放到任務佇列中,而此時會先將迴圈執行完畢再執行 timer
函式,因此當執行 timer
函式時 i
已經等於6了,所以最終會輸出5個6
ps:解決辦法有三種,我只貼程式碼了
//閉包 for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i) } //給setTimeout傳參 //方式一 IE不支援 for (var i = 1; i <= 5; i++) { setTimeout( function timer(j) { console.log(j) }, i * 1000, i ) }
//方式二
for (var i = 1; i <= 5; i++) {
(function(i){
setTimeout(function(){
console.log(i)
},i * 1000)
})(i) }
//ES6 let
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
4.使用settimeout代替setinterval進行間歇呼叫
var executeTimes = 0;
var intervalTime = 500;
var intervalId = null;
// 放開下面的註釋執行setInterval的Demo
intervalId = setInterval(intervalFun,intervalTime);
// 放開下面的註釋執行setTimeout的Demo
// setTimeout(timeOutFun,intervalTime);
function intervalFun(){
executeTimes++;
console.log("doIntervalFun——"+executeTimes);
if(executeTimes==5){
clearInterval(intervalId);
}
}
function timeOutFun(){
executeTimes++;
console.log("doTimeOutFun——"+executeTimes);
if(executeTimes<5){
setTimeout(arguments.callee,intervalTime);
}
}
程式碼比較簡單,我們只是在settimeout的方法裡面又呼叫了一次settimeout,就可以達到間歇呼叫的目的。 setinterval間歇呼叫,是在前一個方法執行前,就開始計時,比如間歇時間是500ms,那麼不管那時候前一個方法是否已經執行完畢,都會把後一個方法放入執行的序列中。這時候就會發生一個問題,假如前一個方法的執行時間超過500ms,加入是1000ms,那麼就意味著,前一個方法執行結束後,後一個方法馬上就會執行,因為此時間歇時間已經超過500ms了。
5.利用settimeout來實現setinterval
function interval(func, w, t){
var interv = function(){
if(typeof t === "undefined" || t-- > 0){
setTimeout(interv, w);
try{
func.call(null);
}
catch(e){
t = 0;
throw e.toString();
}
}
};
setTimeout(interv, w);
};
參考文件:https://blog.csdn.net/weixin_34204057/article/details/89009605
http://www.luyixian.cn/javascript_show_149688.aspx
https://juejin.im/post/5c89fe42e51d455bb15c1ed1
https://www.cnblogs.com/icctuan/p/12103697.html