一、前言
前端實現動畫效果主要有以下幾種方法: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 ) } //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