john resig寫的一篇文章:javascript
原文地址:http://ejohn.org/blog/how-javascript-timers-work/ html
做爲入門者來講,瞭解JavaScript中timer的工做方式是很重要的。一般它們的表現行爲並非那麼地直觀,而這是由於它們都處在一個單一線程中。讓咱們先來看一看三個用來建立以及操做timer的函數。java
var id = setTimeout(fn, delay);
- 初始化一個單一的timer,這個timer將會在必定延時後去調用指定的函數。這個函數(setTimeout)將返回一個惟一的ID,咱們能夠經過這個ID來取消timer。var id = setInterval(fn, delay);
- 與setTimeout相似,只不過它會持續地調用指定的函數(每次都有一個延時),直到timer被取消爲止。clearInterval(id);
, clearTimeout(id);
- 接受一個timer的ID(由上述的兩個函數返回的),而且中止timer的回調事件。要搞明白timer在 內部是怎麼工做的,咱們還須要知道一個很重要的概念::定時器的延時是沒有保證的。因爲全部在遊覽器執行的js都是單線程異步事件(好比鼠標單擊和定時器),執行過程當中只有在有空閒的時候纔會被執行。經過下圖能夠很好的說明這一觀點:面試
在上圖中有不少信息是須要好好去消化下的,徹底理解會讓你對js的異步工做有更好的認識。上面圖表是一維的:垂直表示時間,按毫秒計算。藍色的盒子表示正在執行的部分js。如第一個js塊執行的時間大約爲18ms,鼠標點擊塊執行的時間大約爲11ms,等等。
由於js只能在某一時刻執行一小段代碼(因爲它的單線程天性),這些執行代碼塊中的每一個都會"阻塞"其餘異步事件的進行。這意味着當一個異步事件發生時(如鼠標單擊事件,定時器觸發,或者異步請求完成時),它會排隊等待執行(至於隊列其實是如何排列的,想必各個遊覽器表現都會不同,因此這樣考慮是一個簡化)。
ajax
剛開始,在第一個JavaScript塊中,有兩個timer被 初始化了:一個10ms的setTimeout和一個是10ms的setInterval。因爲timer(這裏的timer指setTimeout中的 timer,而下文中的interval則指setInvertal中的timer)開始的時間,實際上它在第一個代碼塊結束前就已經觸發了。然而請注 意,它並不會立刻執行(事實上因爲單線程的關係,它也無法作到立刻執行)。相反的,這個被延期執行的函數進入隊列中,等待在空閒的時候被執行。編程
此外,在第一個代碼塊裏,咱們看到一個鼠標單擊事件發生。一個js回調函數被綁定於這個異步事件(咱們不知道用戶什麼時候會點擊,因此這被認爲是異步的),但不會被立刻執行,它像開始的那個定時器同樣,會進入隊列等待執行。json
在第一個js代碼塊初始化完成後,遊覽器會馬上詢問:誰正在等待執行啊?這種狀況下,鼠標單擊事件處理函數和定時器回調函數正在等待執行。而後遊覽器挑選一個(鼠標單擊事件處理函數)立刻執行,定時器回調函數繼續等待下一個可能的時間去執行。瀏覽器
注意當鼠標點擊事件正在執行的時候第一次的interval事件也觸發了,與timer一 樣,它的事件也進入隊列等待以後執行。然而,注意,當interval再次觸發的時候(這個時候timer的事件正在執行),這一次它的事件被丟棄了。如 果你在一個大的JavaScript代碼塊正在執行的時候把全部的interval回調函數都囤起來的話,其結果就是在JavaScript代碼塊執行完 了以後會有一堆的interval事件被執行,而執行過程當中不會有間隔。所以,取代的做法是瀏覽器情願先等一等,以確保在一個interval進入隊列的 時候隊列中沒有別的interval。多線程
事實上,咱們能夠在例子中看出:當第三個interval觸發的時候這個interval自身正在執行。這告訴咱們一個重要的事實:interval是無論當前在執行些什麼的,在任何狀況下它都會進入到隊列中去,即便這樣意味着每次回調之間的時間就不許確了。app
最後,當第二個interval回調執行完後,咱們能夠看到隊列已經被清空,沒有什麼須要JavaScript引擎去執行的了。這代表瀏覽器如今等 待一個新的異步事件發生。因而在50ms的時候咱們看到interval又觸發了。這同樣,因爲沒有什麼東西擋住了它的執行,它立刻就觸發了。
讓咱們來看一個例子,這個例子更好地闡釋了setTimeout和setInveral之間的區別。
乍看上去,這兩段代碼在功能上彷佛是相同的,可實際上並不是如此。setTimeout的代碼在前一次的回調執行完後老是至少會有10ms的延時(有 可能會更多,可是絕對不會更少);而setInterval則老是在每10ms的時候嘗試執行一次回調,它無論上一次回調是何時執行的。
咱們在此學到了不少,讓咱們重述一下:
setTimeout和
setInterval在如何執行代碼上有着本質地區別。
以上這些知識是至關重要的。知道JavaScript引擎的工做方式,尤爲是知道它在有不少異步事件發生的時候是怎麼工做的,爲咱們在寫進階的應用程序代碼打下了堅實的基礎。
某人總結:
總結以下:
所以,對於動畫來講,若是單幀的執行時間大於間隔時間,用setTimeout比用setInterval更保險。John Resig在回覆中也代表了這個觀點:
It really depends on the situation – and how the timers are actually being used. setInterval will, most likely, get you more ‘frames’ in the animation but will certainly tax your processor more. A lot of frameworks end up using setTimeout since it degrades more gracefully on slower computers.
一個簡單的測試頁面:timer_test.html(請在Chrome下運行,注意那些零值或接近零的值,setInterval沒有interval了!)
所以,在這種狀況下,採用setTimeout更保險:
setTimeout(function(){ setTimeout(arguments.callee, 10); }, 10);
固然,大部分狀況下,單幀執行時間都小於預設的間隔時間,上面分析的差別,是感受不大出來的。
------------------------
一道JavaScript面試題(setTimeout)
下面的代碼,多久以後會彈出'end'? 爲何?
var t = true;
setTimeout(function(){ t = false; }, 1000);
while(t){ }
alert('end');
這是之前在想有沒辦法實現阻塞javascript線程的時候(即實現sleep方法),想過的一種實現。
很簡單,是吧?
是嗎?
答案是:典型的死循環……js是單線程執行的,while裏面死掉的時候setTimeout裏面的函數是沒機會執行的。
一、簡單的settimeout
setTimeout(function () { while (true) { } }, 1000); setTimeout(function () { alert('end 2'); }, 2000); setTimeout(function () { alert('end 1'); }, 100); alert('end');
執行的結果是彈出‘end’‘end 1’,而後瀏覽器假死,就是不彈出‘end 2’。也就是說第一個settimeout裏執行的時候是一個死循環,這個直接致使了理論上比它晚一秒執行的第二個settimeout裏的函數被阻塞,這個和咱們平時所理解的異步函數多線程互不干擾是不符的。
二、ajax請求回調
接着咱們來測試一下經過xmlhttprequest實現ajax異步請求調用,主要代碼以下:
var xmlReq = createXMLHTTP();//建立一個xmlhttprequest對象 function testAsynRequest() { var url = "/AsyncHandler.ashx?action=ajax"; xmlReq.open("post", url, true); xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlReq.onreadystatechange = function () { if (xmlReq.readyState == 4) { if (xmlReq.status == 200) { var jsonData = eval('(' + xmlReq.responseText + ')'); alert(jsonData.message); } else if (xmlReq.status == 404) { alert("Requested URL is not found."); } else if (xmlReq.status == 403) { alert("Access denied."); } else { alert("status is " + xmlReq.status); } } }; xmlReq.send(null); } testAsynRequest();//1秒後調用回調函數 while (true) { }
在服務端實現簡單的輸出:
private void ProcessAjaxRequest(HttpContext context) { string action = context.Request["ajax"]; Thread.Sleep(1000);//等1秒 string jsonObject = "{\"message\":\"" + action + "\"}"; context.Response.Write(jsonObject); }
理論上,若是ajax異步請求,它的異步回調函數是在單獨一個線程中,那麼回調函數必然不被其餘線程」阻撓「而順利執行,也就是1秒後,它回調執行彈出‘ajax’,但是實際狀況並不是如此,回調函數沒法執行,由於瀏覽器再次由於死循環假死。
結論:根據實踐結果,能夠得出,javascript引擎確實是單線程處理它的任務隊列(能理解成就是普通函數和回調函數構成的隊列嗎?)的。在javascript裏實現異步編程很大程度上就是一種障眼法,單線程的引擎實現多線程的編程,若是要實現一些資源同步互斥之類的操做(一如C#、Java等語言的多線程),我感受真正實現起來根本沒法輕易獲得保證。
補充:如何實現javascript的sleep呢?在stackoverflow上找到一篇javascript sleep,試了一下,效果是有了,可是執行的時候cpu很高,真還不如直接settimeout呢。
轉自:http://www.cnblogs.com/jeffwongishandsome/archive/2011/06/13/2080145.html
容易欺騙別人感情的JavaScript定時器
JavaScript的setTimeout與setInterval是兩個很容易欺騙別人感情的方法,由於咱們開始經常覺得調用了就會按既定的方式執行, 我想很多人都深有同感, 例如
setTimeout(function() { alert('你好!'); }, 0); setInterval(callbackFunction, 100);
認爲setTimeout中的問候方法會當即被執行,由於這並非憑空而說,而是JavaScript API文檔明肯定義第二個參數意義爲隔多少毫秒後,回調方法就會被執行. 這裏設成0毫秒,理所固然就當即被執行了.
同理對setInterval的callbackFunction方法每間隔100毫秒就當即被執行深信不疑!
但隨着JavaScript應用開發經驗不斷的增長和豐富,有一天你發現了一段怪異的代碼而百思不得其解:
div.onclick = function(){ setTimeout(function() { document.getElementById('inputField').focus(); }, 0); };
既然是0毫秒後執行,那麼還用setTimeout幹什麼, 此刻, 堅決的信念已開始動搖.
直到最後某一天 , 你不當心寫了一段糟糕的代碼:
setTimeout(function() { while (true) { } }, 100); setTimeout(function() { alert('你好!'); }, 200); setInterval(callbackFunction, 200);
第一行代碼進入了死循環,但不久你就會發現,第二,第三行並非預料中的事情,alert問候未見出現,callbacKFunction也杳無音訊!
這時你完全迷惘了,這種情景是難以接受的,由於改變長久以來既定的認知去接受新思想的過程是痛苦的,但情事實擺在眼前,對JavaScript真理的探求並不會由於痛苦而中止,下面讓咱們來展開JavaScript線程和定時器探索之旅!
拔開雲霧見月明
出現上面全部誤區的最主要一個緣由是:潛意識中認爲,JavaScript引擎有多個線程在執行,JavaScript的定時器回調函數是異步執行的.
而事實上的,JavaScript使用了障眼法,在多數時候騙過了咱們的眼睛,這裏背光得澄清一個事實:
JavaScript引擎是單線程運行的,瀏覽器不管在何時都只且只有一個線程在運行JavaScript程序.
JavaScript引擎用單線程運行也是有意義的,單線程沒必要理會線程同步這些複雜的問題,問題獲得簡化.
那麼單線程的JavaScript引擎是怎麼配合瀏覽器內核處理這些定時器和響應瀏覽器事件的呢?
下面結合瀏覽器內核處理方式簡單說明.
瀏覽器內核實現容許多個線程異步執行,這些線程在內核制控下相互配合以保持同步.假如某一瀏覽器內核的實現至少有三個常駐線程:javascript引擎線程,界面渲染線程,瀏覽器事件觸發線程,除些之外,也有一些執行完就終止的線程,如Http請求線程,這些異步線程都會產生不一樣的異步事件,下面經過一個圖來闡明單線程的JavaScript引擎與另外那些線程是怎樣互動通訊的.雖然每一個瀏覽器內核實現細節不一樣,但這其中的調用原理都是大同小異.
由圖可看出,瀏覽器中的JavaScript引擎是基於事件驅動的,這裏的事件可看做是瀏覽器派給它的各類任務,這些任務能夠源自JavaScript引擎當前執行的代碼塊,如調用setTimeout添加一個任務,也可來自瀏覽器內核的其它線程,如界面元素鼠標點擊事件,定時觸發器時間到達通知,異步請求狀態變動通知等.從代碼角度看來任務實體就是各類回調函數,JavaScript引擎一直等待着任務隊列中任務的到來.因爲單線程關係,這些任務得進行排隊,一個接着一個被引擎處理.
上圖t1-t2..tn表示不一樣的時間點,tn下面對應的小方塊表明該時間點的任務,假設如今是t1時刻,引擎運行在t1對應的任務方塊代碼內,在這個時間點內,咱們來描述一下瀏覽器內核其它線程的狀態.
t1時刻:
GUI渲染線程:
該線程負責渲染瀏覽器界面HTML元素,當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行.本文雖然重點解釋JavaScript定時機制,但這時有必要說說渲染線程,由於該線程與JavaScript引擎線程是互斥的,這容易理解,由於JavaScript腳本是可操縱DOM元素,在修改這些元素屬性同時渲染界面,那麼渲染線程先後得到的元素數據就可能不一致了.
在JavaScript引擎運行腳本期間,瀏覽器渲染線程都是處於掛起狀態的,也就是說被」凍結」了.
因此,在腳本中執行對界面進行更新操做,如添加結點,刪除結點或改變結點的外觀等更新並不會當即體現出來,這些操做將保存在一個隊列中,待JavaScript引擎空閒時纔有機會渲染出來.
GUI事件觸發線程:
JavaScript腳本的執行不影響html元素事件的觸發,在t1時間段內,首先是用戶點擊了一個鼠標鍵,點擊被瀏覽器事件觸發線程捕捉後造成一個鼠標點擊事件,由圖可知,對於JavaScript引擎線程來講,這事件是由其它線程異步傳到任務隊列尾的,因爲引擎正在處理t1時的任務,這個鼠標點擊事件正在等待處理.
定時觸發線程:
注意這裏的瀏覽器模型定時計數器並非由JavaScript引擎計數的,由於JavaScript引擎是單線程的,若是處於阻塞線程狀態就計不了時,它必須依賴外部來計時並觸發定時,因此隊列中的定時事件也是異步事件.
由圖可知,在這t1的時間段內,繼鼠標點擊事件觸發後,先前已設置的setTimeout定時也到達了,此刻對JavaScript引擎來講,定時觸發線程產生了一個異步定時事件並放到任務隊列中, 該事件被排到點擊事件回調以後,等待處理.
同理, 仍是在t1時間段內,接下來某個setInterval定時器也被添加了,因爲是間隔定時,在t1段內連續被觸發了兩次,這兩個事件被排到隊尾等待處理.
可見,假如時間段t1很是長,遠大於setInterval的定時間隔,那麼定時觸發線程就會源源不斷的產生異步定時事件並放到任務隊列尾而無論它們是否已被處理,但一旦t1和最早的定時事件前面的任務已處理完,這些排列中的定時事件就依次不間斷的被執行,這是由於,對於JavaScript引擎來講,在處理隊列中的各任務處理方式都是同樣的,只是處理的次序不一樣而已.
t1事後,也就是說當前處理的任務已返回,JavaScript引擎會檢查任務隊列,發現當前隊列非空,就取出t2下面對應的任務執行,其它時間依此類推,由此看來:
若是隊列非空,引擎就從隊列頭取出一個任務,直到該任務處理完,即返回後引擎接着運行下一個任務,在任務沒返回前隊列中的其它任務是無法被執行的.
相信您如今已經很清楚JavaScript是否可多線程,也瞭解理解JavaScript定時器運行機制了,下面咱們來對一些案例進行分析:
案例1:setTimeout與setInterval
setTimeout(function() { /* 代碼塊... */ setTimeout(arguments.callee, 10); }, 10); setInterval(function(){ /*代碼塊... */ }, 10);
這兩段代碼看一塊兒效果同樣,其實非也,第一段中回調函數內的setTimeout是JavaScript引擎執行後再設置新的setTimeout定時, 假定上一個回調處理完到下一個回調開始處理爲一個時間間隔,理論兩個setTimeout回調執行時間間隔>=10ms .第二段自setInterval設置定時後,定時觸發線程就會源源不斷的每隔十秒產生異步定時事件並放到任務隊列尾,理論上兩個setInterval回調執行時間間隔<=10.
案例2:ajax異步請求是否真的異步?
不少同窗朋友搞不清楚,既然說JavaScript是單線程運行的,那麼XMLHttpRequest在鏈接後是否真的異步?
其實請求確實是異步的,不過這請求是由瀏覽器新開一個線程請求(參見上圖),當請求的狀態變動時,若是先前已設置回調,這異步線程就產生狀態變動事件放到JavaScript引擎的處理隊列中等待處理,當任務被處理時,JavaScript引擎始終是單線程運行回調函數,具體點即仍是單線程運行onreadystatechange所設置的函數.
-------------------------------
轉自:http://www.cnblogs.com/dolphinX/archive/2013/04/05/2784933.html
setTimeout()和setInterval()常常被用來處理延時和定時任務。setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式,而setInterval()則能夠在每隔指定的毫秒數循環調用函數或表達式,直到clearInterval把它清除。
從定義上咱們能夠看到兩個函數十分相似,只不過前者執行一次,然後者能夠執行屢次,兩個函數的參數也相同,第一個參數是要執行的code或句柄,第二個是延遲的毫秒數。
很簡單的定義,使用起來也很簡單,但有時候咱們的代碼並非按照咱們的想象精確時間被調用的,很讓人困惑
看個簡單的例子,簡單頁面在加載完兩秒後,寫下Delayed alert!
setTimeout('document.write("Delayed alert!");', 2000);
看起來很合理,咱們再看個setInterVal()方法的例子
var num = 0; var i = setInterval(function() { num++; var date = new Date(); document.write(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds() + '<br>'); if (num > 10) clearInterval(i); }, 1000);
頁面每隔1秒記錄一次當前時間(分鐘:秒:毫秒),記錄十次後清除,再也不記錄。考慮到代碼執行時間可能記錄的不是執行時間,但時間間隔應該是同樣的,看看結果
43:38:116 43:39:130 43:40:144 43:41:158 43:42:172 43:43:186 43:44:200 43:45:214 43:46:228 43:47:242 43:48:256
時間間隔幾乎是1000毫秒,但不精確,這是爲何呢?緣由在於咱們對JavaScript定時器存在一個誤解,JavaScript實際上是運行在單線程的環境中的,這就意味着定時器僅僅是計劃代碼在將來的某個時間執行,而具體執行時機是不能保證的,由於頁面的生命週期中,不一樣時間可能有其餘代碼在控制JavaScript進程。在頁面下載完成後代碼的運行、事件處理程序、Ajax回調函數都是使用一樣的線程,實際上瀏覽器負責進行排序,指派某段程序在某個時間點運行的優先級。
咱們把效果放大一下看看,添加一個耗時的任務
function test() { for (var i = 0; i < 500000; i++) { var div = document.createElement('div'); div.setAttribute('id', 'testDiv'); document.body.appendChild(div); document.body.removeChild(div); } } setInterval(test, 10); var num = 0; var i = setInterval(function() { num++; var date = new Date(); document.write(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds() + '<br>'); if (num > 10) clearInterval(i); }, 1000);
咱們又加入了一個定時任務,看看結果
47:9:222 47:12:482 47:16:8 47:19:143 47:22:631 47:25:888 47:28:712 47:32:381 47:34:146 47:35:565 47:37:406
這下效果明顯了,差距甚至都超過了3秒,並且差距很不一致。
咱們能夠能夠把JavaScript想象成在時間線上運行。當頁面載入的時候首先執行的是頁面生命週期後面要用的方法和變量聲明和數據處理,在這以後JavaScript進程將等待更多代碼執行。當進程空閒的時候,下一段代碼會被觸發
除了主JavaScript進程外,還須要一個在進程下一次空閒時執行的代碼隊列。隨着頁面生命週期推移,代碼會按照執行順序添加入隊列,例如當按鈕被按下的時候他的事件處理程序會被添加到隊列中,並在下一個可能時間內執行。在接到某個Ajax響應時,回調函數的代碼會被添加到隊列。JavaScript中沒有任何代碼是當即執行的,但一旦進程空閒則儘快執行。定時器對隊列的工做方式是當特定時間過去後將代碼插入,這並不意味着它會立刻執行,只能表示它儘快執行。
知道了這些後,咱們就能明白,若是想要精確的時間控制,是不能依賴於JavaScript的setTimeout函數的。
使用 setInterval() 建立的定時器可使代碼循環執行,到有指定效果的時候,清除interval就能夠,以下例
var my_interval = setInterval(function () { if (condition) { //.......... } else { clearInterval(my_interval); } }, 100);
但這個方式的問題在於定時器的代碼可能在代碼再次被添加到隊列以前尚未執行完成,結果致使循環內的判斷條件不許確,代碼多執行幾回,之間沒有停頓。不過JavaScript已經解決這個問題,當使用setInterval()時,僅當沒有該定時器的其餘代碼實例時纔將定時器代碼插入隊列。這樣確保了定時器代碼加入到隊列的最小時間間隔爲指定間隔。
這樣的規則帶來兩個問題
爲了不這兩個缺點,咱們可使用setTimeout()來實現重複的定時器
setTimeout(function () { //code setTimeout(arguments.callee, interval); }, interval)
這樣每次函數執行的時候都會建立一個新的定時器,第二個setTimeout()調用使用了agrument.callee 來獲取當前實行函數的引用,並設置另一個新定時器。這樣作能夠保證在代碼執行完成前不會有新的定時器插入,而且下一次定時器代碼執行以前至少要間隔指定時間,避免連續運行。
setTimeout(function () { var div = document.getElementById('moveDiv'); var left = parseInt(div.style.left) + 5; div.style.left = left + 'px'; if (left < 200) { setTimeout(arguments.callee, 50); } }, 50);
這段定時器代碼每次執行的時候,把一個div向右移動5px,當座標大於200的時候中止。