對於瀏覽器內部,大部分操作都是異步的生成事件並添加到JavaScript 引擎線程
的隊列中,然後由JavaScript
引擎線程
進行調度執行。因此瀏覽器的很多事件都是和JavaScript
相結合的,但是也有一些內部的限制。
首先我們非常確定JavaScript
是單線程的,對於瀏覽器來說,一個窗體中只有一個JavaScript
引擎線程
。而其他的行爲,如:渲染、下載等是由單獨的線程進行管理的,且具有不同的優先級。
異步事件
前面提到大多數事件都是異步的,觸發的時候就將回調函數添加到事件隊列。瀏覽器提供了一個內部的迴路,也就是之前所談到的Event Loop
,由它來負責檢查隊列和處理事件、執行函數等。詳細可參考我的前一篇博文。而setTimeout
和setInterval
也是將其需要執行的函數添加到事件隊列。
事件重疊
一些情況下,會有多個事件在同一時間附加到事件隊列裏。
比如,click
事件就會產生兩個額外的事件:mousedown
和mouseup
。其中,mouseup
和click
事件會同時被添加到事件隊列;而mousedown
事件則很有可能會和另外一個事件重疊:focus
。
setTimeout(func, 0) 奇巧淫技
再一次解釋關於0ms
的誤解:如果當前時鐘週期內執行隊列空閒,則立即執行該定時器,將回調函數加入到事件隊列;然後等待下一個時鐘週期,再執行該回調函數。不妨來看看下面的測試。
這段代碼在我的瀏覽器中執行結果如下:
在我本地的Nodejs
環境中執行結果如下:
上面的這個測試只是想說明setTimeout(func, 0)
定時任務的回調函數執行時間是有延遲的,而並不是所謂的立即執行。
因此,我們可以利用setTimeout(func, 0)
來解決事件重疊所產生的負面效果,修正執行順序。
奇巧淫技之一:模擬瀏覽器的事件捕獲
衆所周知,瀏覽器的 DOM 事件都是採用冒泡的方式,只有個別瀏覽器是支持事件捕獲的。而在實際的開發過程中可能存在需要事件捕獲的需求,要求子元素的事件在父元素觸發之後才能觸發。爲了兼容各個瀏覽器,我們不能使用事件捕獲,而setTimeout(func,
0)
在這個時候就很樂意幫忙了。
<input type="button" value="click" id="cbtn"> <div id="result"></div> <script type="text/javascript"> var cbtn = document.getElementById('cbtn') , result = document.getElementById('result'); cbtn.onclick = function(e) { setTimeout(function() { result.innerHTML += 'input click, '; }, 0); }; document.body.onclick = function(e) { result.innerHTML += 'body click -> '; }; </script>
點擊查看運行效果:
奇巧淫技之二:讓瀏覽器更好的工作
大多數情況下,我們可以在瀏覽器的默認行爲之前對事件進行處理,但是有時我們按照常規的思路去做的時候,往往事與願違。比如下面的例子。
<input type="text" id="wordInput"> <script type="text/javascript"> var wordInput = document.getElementById('wordInput'); wordInput.onkeypress = function(e) { this.value = this.value.toUpperCase(); }; </script>
看似一個很簡單的需求:每輸入一個字符,就將其轉換爲大寫。但是上面的代碼完全沒有按照指示去做,不信你試試看:
如果沒有下一次輸入,文本框中的小寫字母永遠都不會轉換爲大寫。Why? 因爲瀏覽器在keypress
事件處理的時候,還沒有將我們輸入的值添加到文本框。於是乎換一個事件來 handle 然後再處理吧,既然鍵按下的時候還木有值,那就等鍵彈起來之後再處理。
<input type="text" id="wordInput"> <script type="text/javascript"> var wordInput = document.getElementById('wordInput'); wordInput.onkeyup = function(e) { this.value = this.value.toUpperCase(); }; </script>
運行試試吧。
大概似乎是可行了,可是仔細觀察就看出問題了。keyup
事件觸發時,文本框已經具備完整的值了,但先是一個小寫的值,鍵完全釋放之後轉變爲大寫。這不科學…這太醜陋...
是時候關門放出setTimeout(func, 0)
了。。。
<input type="text" id="wordInput"> <script type="text/javascript"> var wordInput = document.getElementById('wordInput'); wordInput.onkeypress = function(e) { var self = this; setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); }; </script>
已經完美了。keypress
事件觸發時,將轉換大寫的操作添加到事件隊列,緊接着瀏覽器添加我們輸入的值,然後近乎 0 延遲的執行我們的轉換大寫操作函數。
上面兩個小案例只是冰山一角,so... 合理利用setTimeout(func, 0)
,明天更美好!