setTimout( , 0) 詳解

setTimout( , 0)

1、前言

前端工程師們工做久了,通常都會在某些地方看見過這樣的代碼:javascript

setTimeout(function(){
    // TODO
}, 0);

舉個實例,移動端咱們常常會用的一個庫叫作iScroll來模仿iOS系統裏面的滾動反彈效果,而它的官方文檔裏面就有相似的代碼建議:html

上面其實也說到了setTimeout( , 0)的做用,就是當你改動了DOM後,讓瀏覽器有一點空餘的時間來重繪這個頁面。可能道理你們都懂,可是爲何啊??下面讓咱們經過實例來研究說明setTimeout(, 0)的工做原理。前端

2、stackoverflow解釋翻譯(原文地址)

stackoverflow是程序員的好基友,所以我在那裏翻到了這個解釋,而且對其進行了中文翻譯,英語好的同窗能夠直接到上面原文地址裏查看。整個解釋很是的詳細:

想象一下頁面中有一個 」do something「 按鈕和一個顯示結果的 DIV。java

」do something「 按鈕中點擊事件onclick的回調函數 」LongCalculate()「 中幹了兩件事:git

  1. 執行一個很是耗時的計算(大約3分鐘)。
  2. 把上面計算的結果輸出到結果 DIV 裏面。

如今,你的用戶開始測試這個功能,點擊 「do something」 按鈕,接着頁面就彷佛在3分鐘內什麼也沒幹,用戶煩躁不安,再次點了一下按鈕,又等了一分鐘,也是什麼都沒有發生,而後再次點擊了按鈕。。。程序員

問題很明顯:你須要有一個「狀態」 DIV,用來展現如今進行的狀況。下面展現的最新的處理。github


因此你添加了一個「狀態」 DIV(剛開始是空的),接着調整onclick的回調函數(函數LongCalc()),調整後該回調函數執行如下4個步驟:api

  1. 改變狀態 DIV 的內容爲 「Calculating... may take ~3 minutes」 。
  2. 執行一個很是耗時的計算(大約3分鐘)。
  3. 把上面計算的結果輸出到結果 DIV 裏面。
  4. 改變狀態DIV的內容爲 「Calculation done」。

修改完畢,你興高采烈地叫你的用戶再來測試如下。瀏覽器

他們滿臉不爽的又走過來講,他們點擊按鈕的時候,狀態 DIV 根本都不會顯示 "Calculation..." 這個狀態!!!markdown


你絞盡腦汁,萬思不得其解。到 StackOverflow瘋狂提問(或者閱讀文檔和問Google),接着你發現問題所在了:

瀏覽器把全部事件觸發的待執行任務( UI 任務和 JavaScript 命令)都放到同一個隊列裏面。而且不幸的是,重繪狀態 DIV 的內容爲 」Calculating...「 是一個分離的待執行任務,這個任務會放到隊列的最後面!

下面是你的用戶測試過程當中的事件分解和隊列中的內容:

  • 隊列:[Empty]
  • 事件:點擊按鈕。事件觸發後隊列的內容:[Execute OnClick handler(line 1-4)]
  • 事件:執行回調函數的第一行代碼(也就是改變狀態 DIV 的值)。事件觸發後隊列的內容:[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]請注意當DOM元素改變的瞬間,須要一個新的事件來重繪這個DOM。這個事件經過改變DOM元素觸發,而且會被放到隊列的最後面。
  • 注意!!!注意!!!下面詳細解釋
  • 事件:執行回調函數的第二行代碼(耗時的計算)。事件觸發後隊列的內容:[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • 事件:執行回調函數的第三行代碼(計算結果輸出到結果 DIV )。事件觸發後隊列的內容:[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
  • 事件:執行回調函數的第四行代碼(結果 DIV 的狀態改成 「DONE」 )。事件觸發後隊列的內容:[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • 事件:執行回調函數隱含的return。從隊列中移除 「Execute OnClick handler」,而後執行隊列中的下一個任務。
  • 注意:因爲咱們已經完成了計算,3分鐘已通過去。重繪事件尚未發生!!!
  • 事件:重繪狀態 DIV 的內容爲 「Calculating」 。把這個重繪任務從隊列中去掉。
  • 事件:使用計算的結果重繪結果 DIV 。把這個重繪任務從隊列中去掉。
  • 事件:重繪狀態 DIV 爲 「Done」。把這個重繪任務從隊列中去掉。眼尖的讀者可能注意到在計算完結以後 「Calculating」 在微秒之間一閃而過。
    所以,潛在的問題就是重繪狀態 DIV 這個事件被放到了隊列的最後,放到了耗時3分鐘的計算後面,因此這個重繪在計算完成前都沒有執行。

要解決這個問題,就要使用setTimeout()了。那麼怎樣解決?由於經過setTimeout調用須要長時間執行的代碼的時候,實際上是建立了兩個事件:setTimeout自身的執行事件,和以後才進隊列的代碼執行事件。(因爲 0 秒 timeout)

So, to fix your problem, you modify your onClick handler to be TWO statements (in a new function or just a block within onClick):

  1. 改變狀態 DIV 的內容爲 「Calculating... may take ~3 minutes」 。

  2. 執行setTimeout(),在0秒後執行LongCalc()函數。

    LongCalc()函數基本上和上面的同樣,但明顯地,不用再在裏面改變狀態 DIV 的內容爲 「Calculating」 ,並且計算也不會馬上執行。

因此呢,如今的事件順序和隊列會變成怎樣呢?

  • 隊列:[Empty]
  • 事件:點擊按鈕。事件觸發後隊列的內容:[Execute OnClick handler(status update, setTimeout() call)]
  • 事件:執行onclick回調函數中的第一行(改變狀態 DIV 的值)。事件觸發後隊列的內容:[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
  • 事件:執行onclick回調函數中的第二行(執行 setTimeout )。事件觸發後隊列的內容:[re-draw Status DIV with "Calculating" value]。隊列在0+秒內不會有新事件入棧。
  • 事件:0+秒以後timeout計時器完成計時。事件觸發後隊列的內容:[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
  • 事件:重繪狀態 DIV 的內容爲 」Calculating「。事件觸發後隊列的內容:[execute LongCalc (lines 1-3)]。注意,此次的重繪事件可能會在timeout計時器完成計時以前執行,不過這不要緊。
  • ...
    萬歲 ! 狀態 DIV 在執行計算前成功更新爲 「Calculating...」 !!!

下面是JSFiddle中解釋這個例子的代碼:http://jsfiddle.net/C2YBE/31/

HTML code:

<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>

JavaScript code: (Executed on onDomReady and may require jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    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;
            }
        }
    }
    $(status_div).text('calclation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});

3、結合Timeline工具分析

上面的解釋已經很清楚了,但仍是有點抽象。爲了更進一步的加深對這個原理的理解,我我的使用Chrome的Timeline工具再進行一次分析,也看看有沒有什麼新的發現。

爲了使數據更加清晰,我把上面js中的jQuery代碼都更換爲原生的api。流程內容其實什麼都沒有改變:

var status_ok = document.getElementById('status_ok');
var do_ = document.getElementById('do');
var status = document.getElementById('status');
var do_ok = document.getElementById('do_ok');

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    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.getElementById(status_div).innerText = 'calclation done';
}

// Assign events to buttons
do_.onclick = function() {
    status.innerText = 'calculating....';
    long_running('status');
};

do_ok.onclick = function() {
    status_ok.innerText = 'calculating...';
    window.setTimeout(function() {long_running('status_ok')}, 0);
};

接下來再放上Timeline的兩張事件記錄圖,左邊爲沒有使用setTimeout的,右邊爲使用了setTimeout的:

先看看沒有使用setTimeout時的事件記錄:

  1. 觸發點擊事件。
  2. 執行點擊事件的回調函數。注意,這一步已經包括耗時的計算代碼了。
  3. 只一次Layout事件和Paint事件!,而觸發這些事件的代碼爲:document.getElementById(status_div).innerText = 'calclation done';

這份記錄和stackoverflow中的解釋基本吻合,但還記得上面說過這樣一句嗎:

眼尖的讀者可能注意到在計算完結以後 「Calculating」 在微秒之間一閃而過。

實際狀況是用戶永遠沒可能看到 「Calculating」 這個狀態,由於瀏覽器的優化功能,把兩個重繪操做合併成一個了。


接下來看看使用了setTimeout的狀況:

  1. 觸發點擊事件
  2. 執行點擊事件的回調函數。
  3. 設置一個Timer。這裏能夠看出執行setTimeout的時候會設置一個Timer,setTimeout的回調函數,只有當這個Timer完成倒計時纔會執行回調函數。
  4. 執行由代碼status_ok.innerText = 'calculating...';引發的重繪操做。
  5. Timer計時器倒計時完畢,執行裏面的計算代碼。
  6. 計算完成後,執行由代碼document.getElementById(status_div).innerText = 'calclation done';引發的重繪操做。

所以,咱們就能夠很肯定的說,setTimeout( , 0)的做用其實就是在進行復雜計算前,騰出一點時間讓瀏覽器能夠完成重繪相關的Layout、Paint等操做。

4、setTimeout( ,0)能百分百解決問題嗎?

不知道你們看到這裏有沒有這樣一個疑問:setTimeout( ,0)騰出的時間必定足夠讓瀏覽器執行Layout、Update Layer Tree和Paint等一連串的動做嗎?先給出一個答案,不必定!

在這裏我繼續拋出一張圖,這張圖是我用上面如出一轍的代碼記錄出來的(使用setTimeout的狀況下):

你們注意到紅框裏面的內容了嗎,瀏覽器要繪製 「calculating...」 的最後一步Paint事件前,Timer計時器倒計時完畢,執行計算代碼了!因此最終都沒有Paint出來!執行完計算以後,直接合並重繪操做,顯示內容 「calclation done」 了。因此此次即便是用了setTimeout( , 0)我也是看不到 「calculating...」 這個狀態的。

因此爲了保證每次的顯示效果都正常,你們能夠把setTimeout( , 0)中的倒計時間設置更久,例如20、30又或者200、300。具體應該是多少須要根據咱們重繪DOM的複雜程度來決定。

其實上面我給出的iScroll文檔說明中也說明過這個問題:

Consider that if you have a very complex HTML structure you may give the browser some more rest and raise the timeout to 100 or 200 milliseconds.

This is generally true for all the tasks that have to be done on the DOM. Always give the renderer some rest.

5、總結

最後的總結:使用setTimeout( , 0)可讓咱們在進行復雜運算前騰出時間,使瀏覽器完成渲染頁面相關的操做。進行復雜的渲染時,也要相對的把倒計時的時間延長,以保證有足夠的時間。

(你們還能夠到個人Github上面得到更好的閱讀體驗,由於博客園的markdown樣式太醜了。。。)

(若是對這篇文章有疑問,你們能夠在下面評論,我會盡快給出答覆。)

相關文章
相關標籤/搜索