本文僅是技術驗證,記錄,交流,不針對任何人。有冒犯的地方,請諒解。本文首發於https://vsnail.cn/static/doc/blog/setTimeout.htmlhtml
本想喝着coffee
,看着娃,過一個恬靜的週六。occasionally
,瀏覽到一段代碼,覺的蠻有趣。java
setTimeout(function(){console.log(1)},30)
setTimeout(function(){console.log(2)},10)
setTimeout(function(){console.log(3)},0)
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
複製代碼
估計大部分人都會知道第一個輸出會是0
(若是還不知道爲何0
會先輸出,也沒有關係,看完整篇文章你就會知道了). 可是後面輸出的順序究竟是3>2>1
仍是1>2>3
,估計就有爭議了。web
回手掏,鬼刀一開看不見,走位走位,手裏幹chrome
想知道上面的答案?等着,讓咱們先來看看setTimeout
相關基礎。segmentfault
setTimeout()
方法能夠設置一個定時器,該定時器在定時器到期後執行一個函數或指定的一段代碼。瀏覽器
最簡單的示例:緩存
100ms
後彈出系統對話框。(非嚴謹的,用俚語表述的需求。。。莫怪)數據結構
setTimeout(function(){
alert('走位,走位')
},100)
複製代碼
好簡單的,是不?地球人都知道的東西,再寫就沒意思了。接下來寫點可能會不知道的東東。多線程
let timer = window.setTimeout(fun\[,delay,param1,param2,...]);
複製代碼
咱們經常使用的就兩個參數,估計一個參數,或者多於兩個參數的狀況用的比較少。只有一個參數時,延遲時間默認爲0;有多於兩個參數時,除開第一和第二參數的其餘參數,咱們稱之爲「附加參數」。附加參數都會作爲回調函數的參數傳遞。app
let func = function(a,b){
console.log(a+b);
}
setTimeout(func,100,10,20); //30
複製代碼
防抖:在事件被觸發n秒後再執行回調,若是在這n秒內又被觸發,則從新計時。
function debounce(fn, wait) {
var timer = null;
return function () {
var context = this
var args = arguments
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(function () {
fn.apply(context, args)
}, wait)
}
}
複製代碼
js
中可使用setInterval
開啓輪詢,可是這種存在一個問題就是執行間隔每每就不是你但願的間隔時間。使用setTimeout
構造輪詢能保證每次輪詢的間隔。
咱們都清楚js
是單線程的,意味着js處理大數據的時候,容易處於‘假死’狀態。那麼這個時候,咱們能夠利用setTimeout
進行切片,來避免‘假死’狀態的出現。
let func = function(index){
....
}
for(let i=0,l=10000000000;i<l;i++){
(function(index){
setTimeout(function(){func(index)},0)
})(i)
}
複製代碼
基本setTimeout
經常使用的用法就是這些。
走位完了,讓咱們一塊兒回手掏掏他們的原理和機制。
咱們都知道,現代瀏覽器每一個標籤頁就是一個進程,每一個進程下面又包含了各類線程,好比javaScript
線程,渲染線程,請求線程等等。也就是說js
是單線程的。估計有人要問了爲何js
是單線程呢,爲何不是多線程呢?其實這和js
的用途有關係。做爲瀏覽器腳本語言,JavaScript
的主要用途是與用戶互動,以及操做DOM
。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript
同時有兩個線程,一個線程在某個DOM
節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?因此,爲了不復雜性,從一誕生,JavaScript
就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。爲了利用多核CPU
的計算能力,HTML5
提出Web Worker
標準,容許JavaScript
腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM
。因此,這個新標準並無改變JavaScript
單線程的本質。
OK,js
是單線程,那麼咱們能夠得出setTimeout
絕對不是開啓另外一個線程來實現異步的。那setTimeout
是如何達到異步效果的呢?
在js
中,全部任務都分爲同步任務和異步任務兩大類。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue
)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
(1)全部同步任務都在主線程上執行,造成一個執行棧(
execution context stack
)。
(2)主線程以外,還存在一個"任務隊列"(task queue
)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。
只要主線程空了,就會去讀取"任務隊列",這就是JavaScript
的運行機制。這個過程會不斷重複。
"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop
(事件循環)。
這裏必定要分清楚task queue
和Event loop
概念。以前,發現不少人老是分不清楚task queue
和 Event loop
概念。說setTimeout
原理時,往往有人說到是將回調函數放入到事件隊列裏面,而後。。。。。。;真覺的這樣的說法不太好。
我的推測,每當調用setTimeout
方法,其實是向一個緩存對象寫入一個鍵值對(以數字爲鍵,以回調函數爲值)。當到達指定延遲時間後,纔將回調函數放入到task queue
中,等待進入執行棧。
在回手掏完以後,基本上setTimeout
以及js
運行機制應該大概明白了。經過以上的原理及機制,咱們來分析一下下面的幾個例子(包含以前沒有說道的setTimeout
的注意項):
setTimeout(function(){console.log(1)},30)
setTimeout(function(){console.log(2)},10)
setTimeout(function(){console.log(3)},0)
let now = new Date();
while(new Date() - now<100){
}
console.log(0);
複製代碼
這段代碼在執行棧中執行順序爲:
- 聲明變量
now
;
2. 向setTimeout緩存對象中放入延遲30毫秒執行的回調函數(這個回調函數咱們標記爲func3);
3. 向setTimeout緩存對象中放入延遲10毫秒執行的回調函數(這個回調函數咱們標記爲func2);
4. 向setTimeout緩存對象中放入延遲0毫秒執行的回調函數(這個回調函數咱們標記爲func1);
5. 向變量now賦值當前時間;
6. 一直循環100ms;6.1. 在0ms時,將回調函數func1放入task queue中。
6.2. 在10ms時,將回調函數func2放入task queue中。
6.3. 在30ms時,將回調函數func3放入task queue中。
- 向控制檯打印0;
- 執行棧空閒,從
task queue
中提取第一個任務(func1)。執行完func1後,再從task queue 中提取一個任務(func2)。執行完func2後,再從task queue 中提取一個任務(func3).
以上就是整個代碼的大概執行流程。所以,咱們獲得的打印順序爲 0>3>2>1。這個裏面主要是要理解,調用setTimeout方法並非直接將回調函數放入task queue
中,而是等到到達指定延時後,纔將回調函數放入task queue
中。
也許你常常用幾秒或者幾十秒作延遲時間,估計你不多會想到setTimeout
能設置的最大的延遲時間是多少呢?或者若是超出setTimeout
的最大延遲時長,又會怎麼樣?
在一篇文章上看到過,setTimout
最大延遲時長是用32位有符號數存儲的,所以他的最大值應該是Math.pow(2,31)-1=2147483647
,那麼換算整天,大約就是24.8
天。若是設置的時長大於2147483647
,那麼setTimeout
的延時時長將會自動設置爲0
;
setTimeout(function(){console.log(1)},2147483648)
setTimeout(function(){console.log(2)},2147483647)
複製代碼
你會看到,控制檯會當即輸出1
,而2
卻沒有輸出,若是上面的結論是正確的,要想看到2
,估計要等24.8
天了。嘿嘿,反正我是不許備等的了。。。有想嘗試的兄弟,能夠試了之後告知下。
在MDN上看到這麼一句話,「delay
取默認值0
,意味着「立刻」執行,或者儘快執行。」
也就是說將延時時長設置爲0
,是在有條件的狀況下儘快執行。但真的是0
毫秒就放入task queue
中嗎?
咱們來看段代碼:
setTimeout(function(){
console.log(2)
},2)
setTimeout(function(){
console.log(6)
},6)
setTimeout(function(){
console.log(1)
},1)
setTimeout(function(){
console.log(3)
},3)
setTimeout(function(){
console.log(0)
},0)
複製代碼
這樣的一段代碼,可能會有些人認爲他的輸出結果是:0>1>2>3>6
.實際狀況卻不是這樣的,實際輸出確是1>0>2>3>6
.
有人解釋說,這是由於從執行延時1ms的延時函數,到執行0ms
的延時函數,中間超過了1ms
,致使延時1ms
的回調函數先於延時0ms
的回調函數進入task queue
中。可是這種說法真不能苟同,若是這樣都須要1ms
那麼js
的運行效率也過低了。並且能夠在1ms
的延時函數 和0ms
的延時函數打印時間戳,能夠發現,根本不多是運行超過1ms
致使的結果。
那麼咱們將1ms
的延時函數和0ms
的延時函數任意交換位置能夠發現,誰在前面誰先進入task queue
。那麼能夠大膽推論,其實延時0ms
與延時1ms
是等價的(這個結論是自我推導的,不必定正確)。所以纔有了1>0>2>3>6
輸出順序。
有人會說上面例子輸出結果是由於setTimeout
的最小間隔時長致使的。最小間隔時長,是個很噁心的概念,最初接觸的時候沒有正確理解,致使一度認爲這個最小間隔時長有問題。
咱們來看看最小間隔時長在MDN
上面的解釋。在MDN
上面它不叫「最小間隔時長」,而是叫作「最小延遲時間」。在之前,最小間隔時長一般爲10ms
,如今的現代瀏覽器一般爲4ms
(根據各個瀏覽器的不一樣會有些差別)。一直以來,都被「最小延遲時間」這個名詞所誤導,總認爲延時時長必須大於等於最小延遲時間。可是,各類測試老是實現不了或者驗證不了這個「最小延遲時間」。在讀MDN
的時候,發現它有這麼一句話"這一般是因爲函數嵌套致使(嵌套層級達到必定深度),或者是因爲已經執行的setInterval
的回調函數阻塞致使".
在瀏覽器中,setTimeout()/setInterval() 的每調用一次定時器的最小間隔是4ms,這一般是因爲函數嵌套致使(嵌套層級達到必定深度),或者是因爲已經執行的setInterval的回調函數阻塞致使的
這才煥然大悟,原來「最小延遲時間」是有限制條件的,他的限制條件就是函數嵌套到達必定深度,或者setInterval回調阻塞。
那麼接下來咱們就用一段代碼驗證下:
function doFunc(count){
console.time('time total:')
let timeFunc = function(){
if(count>=0){
setTimeout(timeFunc,0)
count --;
}else{
console.timeEnd('time total:')
}
}
timeFunc()
}
doFunc(10)
複製代碼
若是沒有最小延遲時間的限制,那麼在只有這段代碼的環境下運行,那麼應該會很快運行完。可是在chrome中實際輸出確是33.2451171875ms
。這就直接證實了最小延遲時間的存在,而且觸發他的條件是函數嵌套到達了必定深度。
好吧,我的覺的setTimeout
的小九九也就這些了,沒有再寫下去的必要了。好比setTimeout
回調函數中的this
,怎麼清除當前創建的全部setTimeout
等等。相似這些地球人都知道的事情,估計也不是您看這篇文章的目的了。
走吧,客官們,嗨把刺激戰場嘍。。。。
一、《window.setTimeout》developer.mozilla.org/zh-CN/docs/…
二、《JavaScript 運行機制詳解:再談Event Loop》www.ruanyifeng.com/blog/2014/1…
三、《setTimeout最小間隔4ms的問題》segmentfault.com/q/101000001…
四、《setTimeout初探(一):4ms的真僞》 blog.csdn.net/yiifaa/arti…
五、 《setTimeout的那些事》imweb.io/topic/56ac6…