JavaScript下的setTimeout(fn,0)意味着什麼?

近期在研究異步編程的我對於setTimeout之類的東西異常敏感。在SegmentFault上看到了一個問題《關於SetTimeout時間設爲0時》:提問者讀了一篇文章,原文解釋setTimeout延遲時間爲0時會發生的事情,提問者提出了幾個文章中的幾個疑點。讀了那篇文章以後發現原文的做者對於setTimeout的理解和本身的認知有點出入,因而編寫了相關測試的代碼以求答案。最終編寫了這篇文章。 javascript

本文內容以下:html

  • 原由
  • 單線程的JavaScript
  • setTimeout背後意味着什麼
  • 參考和引用

JavaScript - 前端開發交流羣:377786580前端

原由

上午在SegmentFault上看到了這個問題《關於SetTimeout 時間設爲0時》(注:SegmentFault正在調整備案,如不能訪問,請點擊這裏),原提問者註明了問題來源:《JS setTimeout延遲時間爲0的詳解》。這個問題來源也是轉載的,我後來找到了出處
在問題來源的那篇的文章中(後者),講述了JS是單線程引擎:它把任務放到隊列中,不會同步去執行,必須在完成一個任務後纔開始另一個任務。
然後,轉載的那篇文章列出並補充了原文的栗子:java

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>setTimeout</title>
    <script type="text/javascript">
        function get(id) {
            return document.getElementById(id);
        }
        window.onload = function () {
            //第一個例子:未使用setTimeout
            get('makeinput').onmousedown = function () {
                var input = document.createElement('input');
                input.setAttribute('type', 'text');
                input.setAttribute('value', 'test1');
                get('inpwrapper').appendChild(input);
                input.focus();
                input.select();
            }
            //第二個例子:使用setTimeout
            get('makeinput2').onmousedown = function () {
                var input = document.createElement('input');
                input.setAttribute('type', 'text');
                input.setAttribute('value', 'test1');
                get('inpwrapper2').appendChild(input);
                //setTimeout
                setTimeout(function () {
                    input.focus();
                    input.select();
                }, 0);
            }
            //第三個例子,onkeypress輸入的時候少了一個值
            get('input').onkeypress = function () {
                get('preview').innerHTML = this.value;
            }
        }
    </script>
</head>
<body>
    <h1><code>setTimeout</code></h1>
    <h2>一、未使用 <code>setTimeout</code></h2>
    <button id="makeinput">生成 input</button>
    <p id="inpwrapper"></p>


    <h2>二、使用 <code>setTimeout</code></h2>
    <button id="makeinput2">生成 input</button>
    <p id="inpwrapper2"></p>


    <h2>三、另外一個例子</h2>
    <p>
        <input type="text" id="input" value="" /><span id="preview"></span>
    </p>
</body>
</html>

代碼運行實例請戳這裏
原文中有這麼一段話,描述的有點抽象:面試

JavaScript引擎在執行onmousedown時,因爲沒有多線程的同步執行,不可能同時去處理剛建立元素的focus 和select方法,因爲這兩個方法都不在隊列中,在完成onmousedown後,JavaScript 引擎已經丟棄了這兩個任務,正如第一種狀況。而在第二種狀況中,因爲setTimeout能夠把任務從某個隊列中跳脫成爲新隊列,於是可以獲得指望的結果。編程

我看到這裏就以爲很是不對勁了。由於按照這種任務會被丟棄的說法,那麼只要在事件觸發的函數中再觸發其餘的事件都會被丟棄,瀏覽器是絕對不會這麼作的,因而我編寫了測試代碼:segmentfault

window.onload = function () {
        //第一個例子:未使用setTimeout
        get('makeinput').onmousedown = function () {
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            //按照文中的理論,這裏的click不會被觸發,但它卻成功觸發了
            get('inpwrapper').click();//觸發了inpwrapper的onclick事件
        }
        get('inpwrapper').onclick = function () {
            alert('linkFly');
        };
    }

下面的onclick()最終是執行了:彈出了"linkFly"。瀏覽器

而在轉載的文中爲了引人深思,又提出了第三個例子:多線程

在此,你能夠看看例子 3,它的任務是實時更新輸入的文本,如今請試試,你會發現預覽區域老是落後一拍,好比你輸 a, 預覽區並無出現 a, 在緊接輸入b時,a才鎮定自若地出現。app

而文中最後留給你們的思考的問題,解決方案就是使用setTimeout再次調整瀏覽器的代碼任務運行隊列。

var domInput = get('input');
    domInput.onkeypress = function () {
        setTimeout(function () {
            //第三個例子的問題就這樣就會被解決
            get('preview').innerHTML = domInput.value;
        })
    }

原文和轉載的文章中都對setTimeout(fn,0)進行了思考,但原文指出的問題本質漏洞百出,因此纔出了這篇文章,咱們的正文,如今開始。

單線程的JavaScript

首先咱們來看瀏覽器下的JavaScript:
瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:javascript引擎線程,GUI渲染線程,瀏覽器事件觸發線程。

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

js的單線程在這一段面試代碼中尤其明顯(理解便可,請不要嘗試...瀏覽器會假死的):

var isEnd = true;
        window.setTimeout(function () {
            isEnd = false;//1s後,改變isEnd的值
        }, 1000);
        //這個while永遠的佔用了js線程,因此setTimeout裏面的函數永遠不會執行
        while (isEnd);
        //alert也永遠不會彈出
        alert('end');

在我工做中對js的認識,我的認爲js的任務單位是函數。即,一個函數表示着一個任務,這個函數沒有執行結束,則在瀏覽器中當前的任務即沒有結束。
上面的代碼中,當前任務由於while的執行而形成永遠沒法執行,因此後面的setTimeout也永遠不會被執行。它在瀏覽器的任務隊列中如圖所示:

Browser Event

setTimeout背後意味着什麼

這篇文章一直在使用setTimeout爲咱們展示和理解js單線程的設計,只是它錯誤的使用了Event來進行演示,並過分解讀了Event。
這裏原文和轉載的文章忽略了這些基礎的事件觸發,並且也恰恰挑了兩套自己設計就比較複雜的API:onmouseXXX系和onkeyXXX系。

onKeyXXX系的API觸發順序如圖:

onKeyXXX

而我我的所理解它們對應的功能:

  • onkeydown - 主要獲取和處理當前按下按鍵,例如按下Enter後進行提交。在這一層,並無更新相關DOM元素的值。
  • onkeypress - 主要獲取和處理長按鍵,由於onkeypress在長按鍵盤的狀況下會反覆觸發直到釋放,這裏並無更新相關DOM元素的值,值得注意的是:keypress以後纔會更新值,因此在長按鍵盤反覆觸發onkeypress事件的時候,後一個觸發的onkeypress能獲得上一個onkeypress的值。因此出現了onkeypress每次取值都會是上一次的值而不是最新值。
  • onkeyup - 觸發onkeyup的DOM元素的值在這裏已經更新,能夠拿到最新的值,因此這裏主要處理相關DOM元素的值。

流程就是上面的圖畫的那樣:

onkeydown => onkeypress => onkeyup

使用了setTimeout以後,流程應該是下面這樣子的:

onkeydown => onkeypress => function => onkeyup

使用setTimeout(fn,0)以後,在onkeypress後面插入了咱們的函數function。上面所說,瀏覽器在onkeypress以後就會更新相關DOM元素的狀態(input[type=text]的value),因此咱們的function裏面能夠拿到最新的值。
因此咱們在onkeypress裏面掛起setTimeout能拿到正確的值,下面的代碼能夠測試使用setTimeout(fn,0)以後的流程:

window.onload = function () {
        var domInput = get('input'), view = get('preview');
        //onkeypress兼容性和說明:http://www.w3school.com.cn/jsref/jsref_events.asp
        domInput.onkeypress = function () {
            setTimeout(function () {
                //這個函數在keypress以後,keyup以前執行
                console.log('linkFly');
            });
        };
        domInput.onkeyup = function () {
            console.log('up');
        };
    };

而後咱們再來談談原代碼中的示例1和示例2,示例1和示例2的區別在這裏:

//示例1
        input.focus();
        input.select();
        
        //示例2
        setTimeout(function () {
            input.focus();
            input.select();
        }, 0);

原文章中說示例1的focus()和select()在onmousedown事件中被丟棄,從而致使了沒有選中,但原文的做者忽略了他註冊的事件是:onmousedown。
咱們暫且不討論onmouseXXX系的其餘API,咱們僅關注和點擊相關的,它們的執行順序是:

  • mousedown - 鼠標按鈕按下
  • mouseup - 鼠標按鈕釋放
  • click - 完成單擊

咱們在onmousedown裏面新建了input,而且選中input的值(調用了input.focus(),input.select())。
那麼爲何沒有被選中呢?這樣,咱們來作一次測試,看看咱們的onfocus究竟是被丟棄了,仍是觸發了。咱們把原文的代碼進行改寫:

window.onload = function () {
        var makeBtn = get('makeinput');
        //觀察onmouseXXX系完成整個單擊的順序
        makeBtn.onmousedown = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {//觀察咱們新生成的input何時獲取焦點的,或者它有沒有像原文做者說的那樣被丟棄了
                console.info('input focus');
            };
            input.focus();
            input.select();
        }
        makeBtn.onclick = function (e) {
            console.log(e.type);
        };
        makeBtn.onmouseup = function (e) {
            console.log(e.type);
        };
        makeBtn.onfocus = function () {//觀察咱們生成按鈕何時獲取焦點的
            console.log('makeBtn focus');
        }
    };

代碼運行的結果是這樣的:
onmouseXXX & focus

咱們的input focus執行了——那麼它爲何沒有獲取到焦點呢?咱們再看看後面執行的函數:咱們點擊的按鈕,在mousedown以後,纔得到焦點,也就是說:咱們的input原本已經獲得了focus(),但在onmousedown以後,咱們點擊的按鈕才遲遲觸發了本身的onfocus(),致使咱們的input被覆蓋。
咱們再加上setTimeout進行測試:

window.onload = function () {
        var makeBtn = get('makeinput');
        makeBtn.onmousedown = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {
                console.info('input focus');
            };
            //咱們加上setTimeout,看看會發生什麼
            setTimeout(function () {
                input.focus();
                input.select();
            });
        }
        makeBtn.onclick = function (e) {
            console.log(e.type);
        };
        makeBtn.onmouseup = function (e) {
            console.log(e.type);
        };
        makeBtn.onfocus = function () {
            console.log('makeBtn focus');
        }
    };

執行結果是這樣:
onmouseXXX and settimeout

能夠看見當咱們點擊"生成"按鈕的時候,按鈕的focus正確的執行了,而後才執行了input focus。
在示例1中,咱們在onmousedown()中執行了input.focus()致使input獲得焦點,而onmousedown以後,咱們點擊的按鈕才遲遲獲得了本身的焦點,形成了咱們input剛拿到手還沒焐熱的焦點被轉移。
而示例2中的代碼,咱們延遲了焦點,當按鈕得到焦點以後,咱們的input再把焦點搶過來,因此,使用setTimeout(fn,0)以後,咱們的input能夠獲得焦點並選中文本。
這裏值得思考的focus()的執行時機,根據此次測試觀察,發現focus事件好像掛載在mousedown以內的最後面,而不是直接掛在mousedown的後面。它和mousedown彷彿是一體的。
咱們使用setTimeout以前的任務流程是這樣的(->表示在上一個任務中,=>表示在上一個任務後):

onmousedown -> onmousedown中執行了input.focus() -> button.onfocus => onmouseup => onclick

onmouseXXX事件流程

而咱們使用了setTimeout以後的任務流程是這樣的:

onmousedown -> button.onfocus => input.focus => onmouseup => onclick

onmouseXXX+setTimeout事件流程

而從上面的流程上咱們得知了另外的消息,咱們還能夠把input.focus掛在mouseup和click下,由於在這些事件以前,咱們的按鈕已經獲得過焦點了,不會再搶咱們的焦點了。

makeBtn.click = function (e) {
            console.log(e.type);
            var input = document.createElement('input');
            input.setAttribute('type', 'text');
            input.setAttribute('value', 'test1');
            get('inpwrapper').appendChild(input);
            input.onfocus = function () {//觀察咱們新生成的input何時獲取焦點的
                console.info('input focus');
            };
            input.focus();
            input.select();
        }

咱們應該認識到,利用setTimeout(fn,0)的特性,能夠幫助咱們在某些極端場景下,修正瀏覽器的下一個任務。

到了這裏,咱們已經能夠否認原文所說的:"JavaScript引擎已經丟棄了這兩個任務"。
我仍然相信,瀏覽器是愛咱們的(除了IE6和移動端一些XXOO的瀏覽器!!!!)瀏覽器並不會無緣無故的丟棄咱們辛勞寫下的代碼,多數時候,只是由於咱們沒有看見背後的真相而已。

當咱們踏進計算機的世界寫下"hello world"的時候就應該堅信,這個二進制的世界裏,永遠存在真相。

參考和引用

JavaScript - 前端開發交流羣:377786580

做者:linkFly
聲明:嘿!你都拷走上面那麼一大段了,我以爲你應該也不介意順便拷走這一小段,但願你可以在每一次的引用中都保留這一段聲明,尊重做者的辛勤勞動成果,本文與博客園共享。
相關文章
相關標籤/搜索