JS單線程問題

這個系列的文章名爲“JavaScript 進階”,內容涉及JS中容易忽略但是很有用的,偏JS底層的,以及複雜項目中的JS的實踐。主要來源於我幾年的開發過程中遇到的問題。小弟第一次寫博客,寫的不好的地方請諸位斧正,覺得還有一些閱讀價值的請幫忙分享下。這個“JavaScript 進階”是一個系列文章,請大家鼓勵鼓勵,我儘快更新。另外,如果你有比較好的話題,也可以在下面評論,我們一起研究提高。

JS是多線程的嗎?

多線程編程相信大家都很熟悉,比如在界面開發中,如果一個事件的響應需要較長時間,那麼一般做法就是把事件處理程序寫在另外一個線程中,在處理過程中,在界面上面顯示類似進度條的元素。這樣界面就不會卡住,並且能夠顯示任務執行進度。記得剛開始做前端的時候,老闆交代在界面上面做一個定時器,每秒更新用戶的在線時間。當時擁有Java和C++開發經驗的我自信滿滿的說我加一個線程就可以分分鐘搞定了。所以查閱文檔,發現setTimeout和setInterval可以很方便的實現該功能。那時候我就認爲這就是JS中的多線程。setTimeout相當於啓動一個線程,等待一段時間後執行函數,setInterval則是在另外的一個線程中,每隔一段時間執行函數。這個觀念在我的頭腦中存在了一年左右,直到遇到了這樣的一個問題。

 

測試人員發現一個按鈕的點擊響應時間較長,在響應過程中,界面卡住了,我檢查代碼發現代碼中做了這樣的事情。

 

  1. $("#submit").on("click", function() {  
  2.     bigTask(); // 這個函數需要較長時間來執行  
  3. })  

所以我想很簡單啊,把這個函數放在另外的一個線程執行就好了啊,所以代碼改成了這樣, 以爲可以輕鬆解決問題。但是事實上發現毫無用處,界面還是原來一樣的行爲,點擊按鈕之後卡住了幾秒。

 

 

  1. $("#submit").on("click", function() {  
  2.     setTimeout(function() {  
  3.         bigTask()  
  4.     }, 0)  
  5. })  

 

這個問題我百思不得其解,最後多方查閱資料才明白瞭如下的內容:

 

  1. 瀏覽器中的JS是單線程的。
  2. setInterval和setTimeout並不是多線程,這兩個函數根本上其實是事件觸發函數

想證明setInterval和setTimeout不是多線程很簡單,你可以試試這樣一段代碼

 

  1. setTimeout(function() {  
  2.     while(true){}  
  3. }, 0)  
  4. setTimeout(function() {  
  5.     alert("foo")  
  6. }, 1000)  

 

不出意外,你的瀏覽器無法響應了,頁面上面的按鈕不能點,Gif也不能動,那個alert肯定也出不來。這是爲什麼呢?

爲了解釋上面的問題,我們來深入解析一下瀏覽器。瀏覽器中有三個常駐的線程,分別是JS引擎,界面渲染,事件響應。由於這三個線程同時要訪問DOM樹,所以爲了線程安全,瀏覽器內部需要做互斥:當JS引擎在執行代碼的時候,界面渲染和事件響應兩個線程是被暫停的。所以當JS出現死循環,瀏覽器無法響應點擊,也無法更新界面。現在的瀏覽器的JS引擎都是單線程的,儘管多線程功能強大,但是線程同步比較複雜,並且危險,稍有不慎就會崩潰死鎖。單線程的好處不必考慮線程同步這樣的複雜問題,簡單而安全。下面的一幅圖來簡要說明JS引擎的執行流程:

JS引擎基於事件來執行代碼。事件響應線程在接到事件後,把響應的代碼放到JS引擎的隊列中,JS引擎按順序執行代碼。在JS引擎沒有代碼可以執行的時候,比如圖中藍色方框的間隙中,事件線程和渲染線程得以有機會運行。基於這些信息,能夠的出下面的結論

 

  1. setTimeout,setInterval並不是多線程,只是一個定時的事件觸發器,它們在合適的時間把一些JS代碼塞到JS引擎的隊列中。
  2. setTimeout(aFunction, 0),這行代碼看似的意思是在0秒之後執行aFunction, 但這並不意味着立即執行。其它真正的意思是立刻把aFunction的代碼放到當前JS引擎的隊列中。所以當前代碼塊執行完成之前,aFunction的代碼是得不到執行的。比如這段代碼,一定是world先出來,hello後出來。儘管setTimeout的參數是0,但這並不意味着立即執行
    1. setTimeout(function() {  
    2.     alert("hello");  
    3. }, 0)  
    4. alert("world")  
  3. 在一個事件的響應代碼執行完成之後,即使隊列中有待執行的代碼,瀏覽器也會先執行頁面渲染和響應事件,完成之後再執行隊列中的代碼。

異步Ajax

看到這邊相信各位應該對JS的單線程以及setTimeout,setInterval的本質有所瞭解了,那麼我們再繼續討論下一個問題,異步Ajax。上文說了,JS是單線程的,當一個函數執行的時候,JS引擎會鎖住DOM樹,其他事件的響應代碼只能在隊列中等待,並且此時頁面卡死。那麼異步Ajax是怎麼回事呢?一個常用的開發實踐就是發起一個異步的Ajax,界面顯示一個進度條樣式的Gif,說好的單線程呢?事實上異步Ajax確實用了多線程,只是Ajax請求的Http連接部分由瀏覽器另外開了一個線程執行,執行完畢之後給JS引擎發送一個事件,這時候異步請求的回調代碼得以執行。它的執行流程是這樣的:


Http請求的執行在另外一個線程中,由於這個線程並不會操作DOM樹,所以是可以保證線程安全的。發起Ajax請求和回調函數中間是沒有JS執行的,所以頁面不會卡死。

 

 

真正的多線程JS

 

在HTML5中,引入了Web Worker這個概念。它能夠在另外一個線程中執行計算密集的JS代碼而不引起頁面卡死,這是真正的多線程。然而爲了保證線程安全,Worker中的代碼是不能訪問DOM的。其具體使用方法在此不作贅述,請參考:http://www.w3school.com.cn/html5/html_5_webworkers.asp

總結

結合上面的分析,總結出出下面的一些實踐供各位參考。

    1. 避免編寫計算密集的前端代碼。
    2. 使用異步Ajax。
    3. 避免編寫一個需要較長時間來執行的JS代碼,比如生成一個大型的表。遇到這種情況,可以分批執行,比如用setInterval來每秒生成20行,或是用戶向下拖動滾動條時候再繼續產生新的行。
    4. 在頁面初始化時候不要執行很多的初始化代碼,否則會影響頁面渲染變慢。一些不需要立即執行的代碼可以在頁面渲染完成之後再執行,比如綁定事件,生成菜單之類的控件。
    5. 對於複雜頁面(像淘寶首頁),可以結合異步Ajax分批產生頁面。先生成頁面框架,頁面內容自上而下用異步Ajax逐步加載並填充到框架中。這樣能夠讓用戶更早的看到頁面。
    6. setTimeout(function, 0)是有用的。它可以讓callback作爲另外一個事件響應代碼來執行。實現了當前事件的代碼執行完成之後,再渲染DOM,再執行setTimeout的callback。這樣能夠讓一部分代碼延後執行,並且在這之前渲染DOM。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章