近期在研究異步編程的我對於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引擎線程,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也永遠不會被執行。它在瀏覽器的任務隊列中如圖所示:
這篇文章一直在使用setTimeout爲咱們展示和理解js單線程的設計,只是它錯誤的使用了Event來進行演示,並過分解讀了Event。
這裏原文和轉載的文章忽略了這些基礎的事件觸發,並且也恰恰挑了兩套自己設計就比較複雜的API:onmouseXXX系和onkeyXXX系。
onKeyXXX系的API觸發順序如圖:
而我我的所理解它們對應的功能:
- 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'); } };
代碼運行的結果是這樣的:
咱們的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'); } };
執行結果是這樣:
能夠看見當咱們點擊"生成"按鈕的時候,按鈕的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
而咱們使用了setTimeout以後的任務流程是這樣的:
onmousedown -> button.onfocus => input.focus => onmouseup => onclick
而從上面的流程上咱們得知了另外的消息,咱們還能夠把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