重複定時器,JS有一個方法叫作setInterval專門爲此而生,可是你們diss他的理由不少,好比跳幀,好比容易內存泄漏,是個沒人愛的孩子。並且setTimeout徹底能夠經過自身迭代實現重複定時的效果,所以setIntervval更加無人問津,並且對他遠而避之,感受用setInterval就很low。But!setInverval真的不如setTimeout嗎?請你們跟着筆者一塊兒來一步步探索吧!web
重複定時器存在的問題ajax
手寫一個重複定時器算法
那些年setInterval背的鍋——容易形成內存泄漏chrome
不管是setTimeout仍是setInterval都逃不過執行延遲,跳幀的問題。爲何呢?緣由是事件環中JS Stack過於繁忙的緣由,當排隊輪到定時器的callback執行的時候,早已超時。還有一個緣由是定時器自己的callback操做過於繁重,甚至有async的操做,以致於沒法預估運行時間,從而設定時間。編程
對於setTimeout經過自身迭代實現重複定時的效果這一方法的使用,筆者最先是經過自紅寶書瞭解的。數組
setTimeout(function(){
var div = document.getElementById("myDiv");
left = parseInt(div.style.left) + 5;
div.style.left = left + "px";
if (left < 200){
setTimeout(arguments.callee, 50);
}
}, 50);
複製代碼
選自《JavaScript高級程序設計(第3版)》第611頁瀏覽器
這應該是很是經典的一種寫法了,可是setTimeout自己運行就須要額外的時間運行結束以後再激活下一次的運行。這樣會致使一個問題就是時間不斷延遲,本來是1000ms的間隔,再setTimeout無心識的延遲下也許會慢慢地跑到總時長2000ms的誤差。bash
說到想要修正時間誤差,你們會想到什麼?沒錯!就是獲取當前時間的操做,經過這個操做,咱們就能夠每次運行的時候修復間隔時間,讓總時長不至於誤差太大。websocket
/*
id:定時器id,自定義
aminTime:執行間隔時間
callback:定時執行的函數,返回callback(id,runtime),id是定時器的時間,runtime是當前運行的時間
maxTime:定時器重複執行的最大時長
afterTimeUp:定時器超時以後的回調函數,返回afterTimeUp(id,usedTime,countTimes),id是定時器的時間,usedTime是定時器執行的總時間,countTimes是當前定時器運行的回調次數
*/
function runTimer(id,aminTime,callback,maxTime,afterTimeUp){
//....
let startTime=0//記錄開始時間
function getTime(){//獲取當前時間
return new Date().getTime();
}
/*
diffTime:須要扣除的時間
*/
function timeout(diffTime){//主要函數,定時器本體
//....
let runtime=aminTime-diffTime//計算下一次的執行間隔
//....
timer=setTimeout(()=>{
//....
//計算需扣除的時間,並執行下一次的調用
let tmp=startTime
callback(id,runtime,countTimes);
startTime=getTime()
diffTime=(startTime-tmp)-aminTime
timeout(diffTime)
},runtime)
}
//...
}
複製代碼
重複定時器的啓動很簡單,可是中止並無這麼簡單。咱們能夠經過新建一個setTimeout結束當前的重複定時器,好比值執行20秒鐘,超過20秒就結束。這個處理方案沒有問題,只不過又多給了應用加了一個定時器,多一個定時器就多一個不肯定因素。網絡
所以,咱們能夠經過在每次執行setTimeout的是判斷是否超時,若是超時則返回,並不執行下一次的回調。同理,若是想要經過執行次數來控制也能夠經過這個方式。
function runTimer(id,aminTime,callback,maxTime,afterTimeUp){
//...
function timeout(diffTime){//主要函數,定時器本體
//....
if(getTime()-usedTime>=maxTime){ //超時清除定時器
cleartimer()
return
}
timer=setTimeout(()=>{
//
if(getTime()-usedTime>=maxTime){ //由於不知道那個時間段會超時,因此都加上判斷
cleartimer()
return
}
//..
},runtime)
}
function cleartimer(){//清除定時器
//...
}
function starttimer(){
//...
timeout(0)//由於剛開始執行的時候沒有時間差,因此是0
}
return {cleartimer,starttimer}//返回這兩個方法,方便調用
}
複製代碼
按照次數中止,咱們能夠在每次的callback中判斷。
let timer;
timer=runTimer("a",100,function(id,runtime,counts){
if(counts===2){//若是已經執行兩次了,則中止繼續執行
timer.cleartimer()
}
},1000,function(id,usedTime,counts){})
timer.starttimer()
複製代碼
經過上方按照次數中止定時器的思路,那麼咱們能夠作一個手動中止的方式。建立一個參數,用於監控是否須要中止,若是爲true,則中止定時器。
let timer;
let stop=false
setTimeout(()=>{
stop=true
},200)
timer=runTimer("a",100,function(id,runtime,counts){
if(stop){
timer.cleartimer()
}
},1000,function(id,usedTime,counts){})
timer.starttimer()
複製代碼
你們必定認爲setTimeout高效於setInterval,不過事實啪啪啪打臉,事實勝於雄辯,setInterval反而略勝一籌。不過要將setInterval打形成高性能的重複計時器,由於他之因此這麼多毛病是沒有用對。通過筆者改造後的Interval能夠說和setTimeout不相上下。
將setInterval封裝成和上述setTimeout同樣的函數,包括用法,區別在於setInterval不須要重複調用自身。只須要在回調函數中控制時間便可。
timer=setInterval(()=>{
if(getTime()-usedTime>=maxTime){
cleartimer()
return
}
countTimes++
callback(id,getTime()-startTime,countTimes);
startTime=getTime();
},aminTime)
複製代碼
爲了證實Interval的性能,如下是一波他們兩的pk。
Nodejs中:
瀏覽器中:
在渲染或者計算沒有什麼壓力的狀況下,定時器的效率
在再渲染或者計算壓力很大的狀況下,定時器的效率
首先是毫無壓力的狀況下你們的性能,Interval完勝!
接下來是頗有壓力的狀況下?。哈哈蒼天饒過誰,在相同時間,相同壓力的狀況下,都出現了跳幀超時,不過兩人的緣由不同setTimeout壓根沒有執行
,而setInterval是由於拋棄了相同隊列下相同定時器的其餘callback
也就是隻保留了了隊列中的第一個擠進來的callback,能夠說兩人表現旗鼓至關。
也就是說在同步的操做的狀況下,這二者的性能並沒有多大區別,用哪一個均可以。可是在異步的狀況下,好比ajax輪循(websocket不在討論範圍內),咱們只有一種選擇就是setTimeout,緣由只有一個——天曉得此次ajax要浪多久才肯回來,這種狀況下只有setTimeout才能勝任。
竟然setTimeout不比setInterval優秀,除了使用場景比setInterval廣,從性能上來看,二者不分伯仲。那麼爲何呢?在下一小節會從事件環,內存泄漏以及垃圾回收這幾個方面診斷一下緣由。
爲了弄清楚爲何二者都沒法精準地執行回調函數,咱們要從事件環的特性開始入手。
在進入正題以前,咱們先討論下JS的特性。他和其餘的編程語言區別在哪裏?雖然筆者沒有深刻接觸過其餘語言,可是有一點能夠確定,JS是服務於瀏覽器的,瀏覽器能夠直接讀懂js。
對於JS還有一個高頻詞就是,單線程。那麼什麼是單線程呢?從字面上理解就是一次只能作一件事。好比,學習的時候沒法作其餘事情,只能專心看書,這就是單線程。再好比,有些媽媽很厲害,能夠一邊織毛衣一邊看電視,這就是多線程,能夠同一時間作兩件事。
JS不只是單線程,仍是非阻塞的語言,也就是說JS並不會等待某一個異步加載完成,好比接口讀取,網絡資源加載如圖片視頻。直接掠過異步,執行下方代碼。那麼異步的函數豈不是永遠沒法執行了嗎?
所以,JS該如何處理異步的回調方法?因而eventloop出現了,經過一個無限的循環,尋找符合條件的函數,執行之。可是JS很忙的,若是一直不斷的有task任務,那麼JS永遠沒法進入下一個循環。JS說我好累,我不幹活了,罷工了。
因而出現了stack和queue,stack是JS工做的堆,一直不斷地完成工做,而後將task推出stack中。而後queue(隊列)就是下一輪須要執行的task們,全部未執行而將執行的task都將推入這個隊列之中。等待當前stack清空執行完畢,而後eventloop循環至queue,再將queue中的task一個個推到stack中。
正由於eventloop循環的時間按照stack的狀況而定。就像公交車同樣,一站一站之間的時間雖然能夠預估,可是不免有意外發生,好比堵車,好比乘客太多致使上車時間過長,好比不當心每一個路口都吃到了紅燈等等意外狀況,都會致使公交陳晚點。eventloop的stack就是一個不定因素,也許stack內的task都完成後遠遠超過了queue中的task推入的時間,致使每次的執行時間都有誤差。
說到內存泄漏就不得不說起垃圾回收(garbage collection),這兩個概念綁在一塊兒解釋比較好,但是說是一對好基友。什麼是內存泄露?聽上去特別牛逼的概念,其實就是咱們建立的變量或者定義的對象,沒有用了以後沒有被系統回收,致使系統沒有新的內存分配給以後須要建立的變量。簡單的說就是借了沒還,債臺高築。因此垃圾回收的算法就是來幫助回收這些內存的,不過有些內容應用不須要,然而開發者並無釋放他們,也就是我不須要了可是死活不放手,垃圾回收也沒辦法只能略過他們去收集已經被拋棄的垃圾。那麼咱們要怎樣才能告訴垃圾回收算法,這些東西我不要了,你拿走吧?怎麼樣的辣雞才能被回收給新辣雞騰出空間呢?說到底這就是一個編程習慣的問題。
致使memory leak的最終緣由只有一個,就是沒有即便釋放不須要的內存——也就是沒有釋放定義的參數,致使垃圾回收沒法回收內存,致使內存泄露。
那麼內存是怎麼分配的呢?
好比咱們定義了一個常量var a="apple"
,那麼內存中就會分配出空間村粗apple這個字符串。你們也許會以爲不就是字符串嘛,能佔多少內存。沒錯,字符串佔不了多少內存,可是若是是一個成千上萬的數組呢?那內存佔的可就不少了,若是不及時釋放,後續工做會很艱難。
可是內存的概念太過於抽象,該怎麼才能feel到這個佔了多少內存或者說內存被釋放了呢?打開chrome的Memory神器,帶你體驗如何感受內存。
這裏咱們建立一個demo用來測試內存是如何工做的:
let array=[]//建立數組
createArray()//push內容,增長內存
function createArray(){
for(let j=0;j<100000;j++){
array.push(j*3*5)
}
}
function clearArray(){
array=[]
}
let grow=document.getElementById("grow")
grow.addEventListener("click",clearArray)//點擊清除數組內容,也就是清除了內存
複製代碼
實踐是惟一獲取真理的方式。經過chrome的測試工具,咱們能夠發現清除分配給變量的內容,能夠釋放內存,這也是爲何有許多代碼結束以後會xxx=null
,也就是爲了釋放內存的緣由。
既然咱們知道了內存是如何釋放的,那麼什麼狀況,即便咱們清空了變量也沒法釋放的內存的狀況呢?
作了一組實驗,array分別爲函數內定義的變量,以及全局變量
let array=[]
createArray()
function createArray(){
for(let j=0;j<100000;j++){
array.push(j*3*5)
}
}
複製代碼
createArray()
function createArray(){
let array=[]
for(let j=0;j<100000;j++){
array.push(j*3*5)
}
}
複製代碼
結果驚喜不驚喜,函數運行完以後,內部的內存會自動釋放,無需重置,然而全局變量卻一直存在。也就是說變量的提高(hoist)並且不及時清除引用的狀況下會致使內存沒法釋放。
還有一種狀況與dom有關——建立以及刪除dom。有一組很經典的狀況就是遊離狀的dom沒法被回收。如下的代碼,root已經被刪除了,那麼root中的子元素是否能夠被回收?
let root=document.getElementById("root")
for(let i=0;i<2000;i++){
let div=document.createElement("div")
root.appendChild(div)
}
document.body.removeChild(root)
複製代碼
答案是no,由於root的引用還存在着,雖然在dom中被刪除了,可是引用還在,這個時候root的子元素就會以遊離狀態的dom存在,並且沒法被回收。解決方案就是root=null
,清空引用,消除有力狀態的dom。
若是setInterval中存在沒法回收的內容,那麼這一部份內存就永遠沒法釋放,這樣就致使內存泄漏。因此仍是編程習慣的問題,內存泄漏?setInterval不背這個鍋。
討論完那些緣由會形成內存泄漏,垃圾回收機制。主要分爲兩種:reference-counting和mark sweap。
這個比較容易理解,就是當前對象是否被引用,若是被引用標記。最後沒有被標記的則清除。這樣有個問題就是程序中兩個不須要的參數互相引用,這樣兩個都會被標記,而後都沒法被刪除,也就是鎖死了。爲了解決這個問題,因此出現了標記清除法(mark sweap)。
標記清除法(mark sweap),這個方法是從這個程序的global開始,被global引用到的參數則標記。最後清除全部沒有被標記的對象,這樣能夠解決兩對象互相引用,沒法釋放的問題。
由於是從global開始標記的,因此函數做用域內的變量,函數完成以後就會釋放內存。
經過垃圾回收機制,咱們也能夠發現,global中定義的內容要謹慎,由於global至關因而主函數,瀏覽器不會隨便清除這一部分的內容。因此要注意,變量提高問題。
並無找到石錘代表setInterval是形成內存泄漏的緣由。內存泄漏的緣由分明是編碼習慣很差,setInterval不背這個鍋。