天天看點

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

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

本文内容如下: 起因 單線程的JavaScript setTimeout背後意味着什麼 參考和引用
JavaScript - 前端開發交流群:377786580

在問題來源的那篇的文章中(後者),講述了JS是單線程引擎:它把任務放到隊列中,不會同步去執行,必須在完成一個任務後才開始另外一個任務。

而後,轉載的那篇文章列出并補充了原文的栗子:

原文中有這麼一段話,描述的有點抽象:

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

我看到這裡就覺得非常不對勁了。因為按照這種任務會被丢棄的說法,那麼隻要在事件觸發的函數中再觸發其他的事件都會被丢棄,浏覽器是絕對不會這麼做的,于是我編寫了測試代碼:

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

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

在此,你可以看看例子 3,它的任務是實時更新輸入的文本,現在請試試,你會發現預覽區域總是落後一拍,比如你輸 a, 預覽區并沒有出現 a, 在緊接輸入b時,a才不慌不忙地出現。

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

原文和轉載的文章中都對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的單線程在這一段面試代碼中尤為明顯(了解即可,請不要嘗試...浏覽器會假死的):

在我工作中對js的認識,個人認為js的任務機關是函數。即,一個函數表示着一個任務,這個函數沒有執行結束,則在浏覽器中目前的任務即沒有結束。

上面的代碼中,目前任務因為while的執行而造成永遠無法執行,是以後面的setTimeout也永遠不會被執行。它在浏覽器的任務隊列中如圖所示:

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

這篇文章一直在使用setTimeout為我們展現和了解js單線程的設計,隻是它錯誤的使用了Event來進行示範,并過度解讀了Event。

這裡原文和轉載的文章忽略了這些基礎的事件觸發,而且也偏偏挑了兩套本身設計就比較複雜的API:onmouseXXX系和onkeyXXX系。

onKeyXXX系的API觸發順序如圖:

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

而我個人所了解它們對應的功能:

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)之後的流程:

然後我們再來談談原代碼中的示例1和示例2,示例1和示例2的差別在這裡:

原文章中說示例1的focus()和select()在onmousedown事件中被丢棄,進而導緻了沒有選中,但原文的作者忽略了他注冊的事件是:onmousedown。

我們暫且不讨論onmouseXXX系的其他API,我們僅關注和點選相關的,它們的執行順序是:

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

我們在onmousedown裡面建立了input,并且選中input的值(調用了input.focus(),input.select())。

那麼為什麼沒有被選中呢?這樣,我們來做一次測試,看看我們的onfocus到底是被丢棄了,還是觸發了。我們把原文的代碼進行改寫:

代碼運作的結果是這樣的:

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

我們的input focus執行了——那麼它為什麼沒有擷取到焦點呢?我們再看看後面執行的函數:我們點選的按鈕,在mousedown之後,才獲得焦點,也就是說:我們的input本來已經得到了focus(),但在onmousedown之後,我們點選的按鈕才遲遲觸發了自己的onfocus(),導緻我們的input被覆寫。

我們再加上setTimeout進行測試:

執行結果是這樣:

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

可以看見當我們點選"生成"按鈕的時候,按鈕的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
JavaScript下的setTimeout(fn,0)意味着什麼?

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

onmousedown -> button.onfocus => input.focus => onmouseup => onclick
JavaScript下的setTimeout(fn,0)意味着什麼?

而從上面的流程上我們得知了另外的消息,我們還可以把input.focus挂在mouseup和click下,因為在這些事件之前,我們的按鈕已經得到過焦點了,不會再搶我們的焦點了。

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

到了這裡,我們已經可以否定原文所說的:"JavaScript引擎已經丢棄了這兩個任務"。

我仍然相信,浏覽器是愛我們的(除了IE6和移動端一些XXOO的浏覽器!!!!)浏覽器并不會平白無故的丢棄我們辛勞寫下的代碼,多數時候,隻是因為我們沒有看見背後的真相而已。

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

<a href="http://www.cnblogs.com/zhaodongyu/p/3922961.html">JavaScript異步機制</a> <a href="http://www.ruanyifeng.com/blog/2013/10/event_loop://www.cnblogs.com/zhaodongyu/p/3922961.html">什麼是 Event Loop</a> <a href="http://blog.csdn.net/kongls08/article/details/6996518">javascript線程解釋</a>

作者:linkFly

聲明:嘿!你都拷走上面那麼一大段了,我覺得你應該也不介意順便拷走這一小段,希望你能夠在每一次的引用中都保留這一段聲明,尊重作者的辛勤勞動成果,本文與部落格園共享。

繼續閱讀