javascript線程解釋(setTimeout,setInterval你不知道的事)


john resig寫的一篇文章:

 原文地址:http://ejohn.org/blog/how-javascript-timers-work/ 

作為入門者來說,了解JavaScript中timer的工作方式是很重要的。通常它們的表現行為並不是那么地直觀,而這是因為它們都處在一個單一線程中。讓我們先來看一看三個用來創建以及操作timer的函數。

  • 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只能在某一時刻執行一小段代碼(由於它的單線程天性),這些執行代碼塊中的每個都會"阻塞"其他異步事件的進行。這意味着當一個異步事件發生時(如鼠標單擊事件,定時器觸發,或者異步請求完成時),它會排隊等待執行(至於隊列實際上是如何排列的,想必各個游覽器表現都會不一樣,所以這樣考慮是一個簡化)。
 

 

剛開始,在第一個JavaScript塊中,有兩timer被 初始化了:一個10ms的setTimeout和一個是10ms的setInterval。由於timer(這里的timer指setTimeout中的 timer,而下文中的interval則指setInvertal中的timer)開始的時間,實際上它在第一代碼塊結束前就已觸發了。然而請注 意,它並不會馬上執行(事實上由於單線程的關系,它也無法做到馬上執行)。相反的,這個被延期執行的函數進入隊列中,等待在空閑的時候被執行。


   此外,在第一個代碼塊里,我們看到一個鼠標單擊事件發生。一個js回調函數被綁定於這個異步事件(我們不知道用戶何時會點擊,所以這被認為是異步的),但不會被馬上執行,它像開始的那個定時器一樣,會進入隊列等待執行。

   在第一個js代碼塊初始化完成后,游覽器會立刻詢問:誰正在等待執行啊?這種情況下,鼠標單擊事件處理函數和定時器回調函數正在等待執行。然后游覽器挑選一個(鼠標單擊事件處理函數)馬上執行,定時器回調函數繼續等待下一個可能的時間去執行。

   

   注意當鼠標點擊事件正在執行的時候第一次的interval事件也觸發了,與timer一 樣,它的事件也進入隊列等待之后執行。然而,注意,當interval再次觸發的時候(這個時候timer的事件正在執行),這一次它的事件被丟棄了。如 果你在一個大的JavaScript代碼塊正在執行的時候把所有的interval回調函數都囤起來的話,其結果就是在JavaScript代碼塊執行完 了之后會有一堆的interval事件被執行,而執行過程中不會有間隔。因此,取代的作法是瀏覽器情願先等一等,以確保在一個interval進入隊列的 時候隊列中沒有別的interval。

  事實上,我們可以在例子中看出:當第三個interval觸發的時候這個interval自身正在執行。這告訴我們一個重要的事實:interval是不管當前在執行些什么的,在任何情況下它都會進入到隊列中去,即使這樣意味着每次回調之間的時間就不准確了

  最后,當第二個interval回調執行完后,我們可以看到隊列已經被清空,沒有什么需要JavaScript引擎去執行的了。這表明瀏覽器現在等 待一個新的異步事件發生。於是在50ms的時候我們看到interval又觸發了。這一樣,由於沒有什么東西擋住了它的執行,它馬上就觸發了。

讓我們來看一個例子,這個例子更好地闡釋了setTimeout和setInveral之間的區別。

  setTimeout ( function ( ) {
     // 一個很長的代碼塊……
   setTimeout (arguments. callee10 );
   }10 );
 
  setInterval ( function ( ) {
     // 一個很長的代碼塊…… 
   }10 );
 

乍看上去,這兩段代碼在功能上似乎是相同的,可實際上並非如此。setTimeout的代碼在前一次的回調執行完后總是至少會有10ms的延時(有 可能會更多,但是絕對不會更少);而setInterval則總是在每10ms的時候嘗試執行一次回調,它不管上一次回調是什么時候執行的。

我們在此學到了很多,讓我們重述一下:

  • JavaScript引擎只有一個線程,這使得異步事件必需列隊等待執行。
  • setTimeout和setInterval在如何執行代碼上有着本質地區別。
  • 如果一個timer在將要執行的時候被阻塞,它將會等待下一個時機(比預期的延時要長)。
  • 如果interval的執行時間較長(比指定的延時長),那么它們將連續地執行而沒有延時。

以上這些知識是相當重要的。知道JavaScript引擎的工作方式,尤其是知道它在有很多異步事件發生的時候是怎么工作的,為我們在寫進階的應用程序代碼打下了堅實的基礎。

某人總結:

總結如下:

  • JavaScript engines only have a single thread, forcing asynchronous events to queue waiting for execution.
  • setTimeout and setInterval are fundamentally different in how they execute asynchronous code.
  • If a timer is blocked from immediately executing it will be delayed until the next possible point of execution (which will be longer than the desired delay).
  • Intervals may execute back-to-back with no delay if they take long enough to execute (longer than the specified delay).

 

  1. 無論是setTimeout還是setInterval,觸發時,如果當前進程不為空,都得去排隊等待執行,這一點上是無差異的。
  2. 區別是,setTimeout只需排一次隊,setInterval則需要按照預設的間隔時間,每到時間點都去排一下。
  3. setInterval去排隊時,如果發現自己還在隊列中未執行,則會被drop掉。也就是說,同一個inerval,在隊列里只會有一個。
  4. 因為隊列機制,無論是setTimeout還是setInterval,第一次觸發時的時間,只會等於大於預設時間,不可能小於。
  5. 對於setInterval來說,如果執行時間大於預設間隔時間,很可能導致連續執行,中間沒有時間間隔,這是很糟糕的,很可能會耗費大量cpu.

因此,對於動畫來說,如果單幀的執行時間大於間隔時間,用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里面的函數是沒機會執行的。

1、簡單的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里的函數被阻塞,這個和我們平時所理解的異步函數多線程互不干擾是不符的。

2、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定時器

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引擎與另外那些線程是怎樣互動通信的.雖然每個瀏覽器內核實現細節不同,但這其中的調用原理都是大同小異.


Js線程圖示

由圖可看出,瀏覽器中的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()時,僅當沒有該定時器的其他代碼實例時才將定時器代碼插入隊列。這樣確保了定時器代碼加入到隊列的最小時間間隔為指定間隔。

這樣的規則帶來兩個問題

  1. 1. 某些間隔會被跳過
  2. 2.多個定時器的代碼執行之間的間隔可能比預期要小

為了避免這兩個缺點,我們可以使用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的時候停止。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM