js setTimeOut()

setTimeout,前端工程師一定會打交道的一個函數.它看上去很是的簡單,樸實.有着一個很不平凡的名字--定時器.讓年少的我天真的覺得本身能夠操縱將來.殊不知樸實之中隱含着驚天大密.我還記得我第一次用這個函數的時候,我天真的覺得它就是js實現多線程的工具.當時用它實現了一個坦克大戰的小遊戲,玩兒不亦樂乎.但是隨着在前端這條路上越走越遠,對它理解開始產生了變化.它彷佛開始蒙上了面紗,時常有一些奇怪的表現讓我捉摸不透.終於,個人耐心耗盡,下定決心,要撕開它的面具,一探究竟.javascript

要說setTimeout的淵源,就得從它的官方定義提及.w3c是這麼定義的html

setTimeout() 方法用於在指定的毫秒數後調用函數或計算表達式。前端

看到這樣一個說明,咱們明白了它就是一個定時器,咱們設定的函數就是一個"鬧鐘",時間到了它就會去執行.然而聰明的你不由有這樣一個疑問,若是是settimeout(fn,0)呢?按照定義的說明,它是否會立馬執行?實踐是檢驗真理的惟一標準,讓咱們來看看下面的實驗java

複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    
    <script>
        alert(1); 
        setTimeout("alert(2)", 0); 
        alert(3); 
    </script>
</body>
</html>
複製代碼

這是一個很簡單的實驗,若是settimeout(0)會當即執行,那麼這裏的執行結果就應該是1->2>3  . 然而實際的結果倒是1->3->2. 這說明了settimeout(0)並非當即執行.同時讓咱們對settimeout的行爲感到很詭異.ajax

 

js引擎是單線程執行的

 

咱們先把上面的問題放一放.從js語言的設計上來看看是否能找到蛛絲馬跡.編程

咱們發現js語言設計的一個很重要的點是,js是沒有多線程的.js引擎的執行是單線程執行.這個特性曾經困擾我好久,我想不明白既然js是單線程的,那麼是誰來爲定時器計時的?是誰來發送ajax請求的?我陷入了一個盲區.即將js等同於瀏覽器.咱們習慣了在瀏覽器裏面執行代碼,卻忽略了瀏覽器自己.js引擎是單線程的,但是瀏覽器卻能夠是多線程的,js引擎只是瀏覽器的一個線程而已.定時器計時,網絡請求,瀏覽器渲染等等.都是由不一樣的線程去完成的. 口說無憑,我們依然看一個例子瀏覽器

複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    
</body>
<script>
    var isEnd = true;
    window.setTimeout(function () {
        isEnd = false;//1s後,改變isEnd的值
    }, 1000);
    while (isEnd);
    alert('end');
</script>
</html>
複製代碼

isEnd默認是true的,在while中是死循環的.最後的alert是不會執行的. 我添加了一個定時器,1秒後將isEnd改成false. 若是說js引擎是多線程的,那麼在1秒後,alert就會被執行.然而實際狀況是,頁面會永遠死循環下去.alert並無執行.這很好的證實了,settimeout並不能做爲多線程使用.js引擎執行是單線程的.網絡

 

event loop

 

從上面的實驗中,咱們更加疑惑了,settimeout到底作了什麼事情呢?前端工程師

原來仍是得從js語言的設計上尋找答案.多線程

js引擎單線程執行的,它是基於事件驅動的語言.它的執行順序是遵循一個叫作事件隊列的機制.從圖中咱們能夠看出,瀏覽器有各類各樣的線程,好比事件觸發器,網絡請求,定時器等等.線程的聯繫都是基於事件的.js引擎處理到與其餘線程相關的代碼,就會分發給其餘線程,他們處理完以後,須要js引擎計算時就是在事件隊列裏面添加一個任務. 這個過程當中,js並不會阻塞代碼等待其餘線程執行完畢,並且其餘線程執行完畢後添加事件任務告訴js引擎執行相關操做.這就是js的異步編程模型.

如此咱們再回過頭來看settimeout(0)就會恍然大悟.js代碼執行到這裏時,會開啓一個定時器線程,而後繼續執行下面的代碼.該線程會在指定時間後往事件隊列裏面插入一個任務.由此可知settimeout(0)裏面的操做會放在全部主線程任務以後. 這也就解釋了爲何第一個實驗結果是1->3-2 .

因而可知官方對於settimeout的定義是有迷惑性的.應該給一個新的定義:

在指定時間內, 將任務放入事件隊列,等待js引擎空閒後被執行.

js引擎與GUI引擎是互斥的

 

談到這裏,就不得不說瀏覽器的另一個引擎---GUI渲染引擎. 在js中渲染操做也是異步的.好比dom操做的代碼會在事件隊列中生成一個任務,js執行到這個任務時就會去調用GUI引擎渲染.

js語言設定js引擎與GUI引擎是互斥的,也就是說GUI引擎在渲染時會阻塞js引擎計算.緣由很簡單,若是在GUI渲染的時候,js改變了dom,那麼就會形成渲染不一樣步. 咱們須要深入理解js引擎與GUI引擎的關係,由於這與咱們平時開發息息相關,咱們時長會遇到一些很奇葩的渲染問題.看這個例子

複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <table border=1>
        <tr><td><button id='do'>Do long calc - bad status!</button></td>
            <td><div id='status'>Not Calculating yet.</div></td>
        </tr>
        <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
            <td><div id='status_ok'>Not Calculating yet.</div></td>
        </tr>
    </table>    
<script>

function long_running(status_div) {

    var result = 0;
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    document.querySelector(status_div).innerHTML = 'calclation done' ;
}

document.querySelector('#do').onclick = function () {
    document.querySelector('#status').innerHTML = 'calculating....';
    long_running('#status');
};

document.querySelector('#do_ok').onclick = function () {
    document.querySelector('#status_ok').innerHTML = 'calculating....';
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
};

</script>
</body>
</html>
複製代碼

 咱們但願能看到計算的每個過程,咱們在程序開始,計算,結束時,都執行了一個dom操做,插入了表明當前狀態的字符串,Not Calculating yet.和calculating....和calclation done.計算中是一個耗時的3重for循環. 在沒有使用settimeout的時候,執行結果是由Not Calculating yet 直接跳到了calclation done.這顯然不是咱們但願的.而形成這樣結果的緣由正是js的事件循環單線程機制.dom操做是異步的,for循環計算是同步的.異步操做都會被延遲到同步計算以後執行.也就是代碼的執行順序變了.calculating....和calclation done的dom操做都被放到事件隊列後面並且緊跟在一塊兒,形成了丟幀.沒法實時的反應.這個例子也告訴了咱們,在須要實時反饋的操做,如渲染等,和其餘相關同步的代碼,要麼一塊兒同步,要麼一塊兒異步才能保證代碼的執行順序.在js中,就只能讓同步代碼也異步.即給for計算加上settimeout.

 

settimeout(0)的做用

 

不一樣瀏覽器的實現狀況不一樣,HTML5定義的最小時間間隔是4毫秒. 使用settimeout(0)會使用瀏覽器支持的最小時間間隔.因此當咱們須要把一些操做放到下一幀處理的時候,咱們一般使用settimeout(0)來hack.

 

requestAnimationFrame

 

這個函數與settimeout很類似,但它是專門爲動畫而生的.settimeout常常被用來作動畫.咱們知道動畫達到60幀,用戶就沒法感知畫面間隔.每一幀大約16毫秒.而requestAnimationFrame的幀率恰好是這個頻率.除此以外相比於settimeout,還有如下的一些優勢:

 

  • requestAnimationFrame 會把每一幀中的全部DOM操做集中起來,在一次重繪或迴流中就完成,而且重繪或迴流的時間間隔牢牢跟隨瀏覽器的刷新頻率,通常來講,這個頻率爲每秒60幀,每幀大約16毫秒.
  • 在隱藏或不可見的元素中,requestAnimationFrame將不會進行重繪或迴流,這固然就意味着更少的的cpu,gpu和內存使用量。
  • 但它優於setTimeout/setInterval的地方在於它是由瀏覽器專門爲動畫提供的API,在運行時瀏覽器會自動優化方法的調用,而且若是頁面不是激活狀態下的話,動畫會自動暫停,有效節省了CPU開銷。

 

 

總結:

 

  1. 瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:javascript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。
  2. javascript引擎是基於事件驅動單線程執行的.JS引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JS線程在運行JS程序。
  3. 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行。但須要注意 GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
  4. 當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeOut、也可來自瀏覽器內核的其餘線程如鼠標點擊、AJAX異步請求等,但因爲JS的單線程關係全部這些事件都得排隊等待JS引擎處理。

 

 

 
 
標籤:  settimeouteventloopGUI異步
相關文章
相關標籤/搜索