JavaScript單線程在上篇博客《Promise的前世今生和妙用技巧》的開篇中,我們曾簡述了JavaScript的單線程機制和瀏覽器的事件模型。應很多網友的回復,在這篇文章中將繼續展開這一個話題。當然這里是博主的一些理解,如果還存在什么紕漏的話,請不吝指教。 JavaScript這門語言運行在瀏覽器中,是以單線程的方式運行的。說到單線程,就得從操作系統進程開始說起。進程和線程都是操作系統的概念。進程是應用程序的執行實例,每一個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程中能夠申請創建和使用系統資源(如獨立的內存區域等),這些資源也會隨著進程的終止而被銷毀。而線程則是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的,所以在多線程的情況下,需要特別注意對臨界資源的訪問控制。在系統創建進程之后就開始啟動執行進程的主線程,而進程的生命周期和這個主線程的生命周期一致,主線程的推出也就意味著進程的終止和銷毀。主線程是由系統進程所創建的,同時用戶也可以自主創建其它線程,這一系列的線程都會并發地運行于同一個進程中。 在多線程操作的情況下可以實現應用的并行處理,而提高整個應用程序的性能和吞吐量,更大粒度的榨取本機的CPU利用率,特別是現代很多語言都支持了多核并行處理技術。然后JavaScript居然還是單線程執行,為什么呢? 這是因為JavaScript這門腳本語言誕生的使命所致:JavaScript為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JavaScript是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突;在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,而線程1要求瀏覽器刪除DOM節點,線程2卻希望修改這個節點的某些樣式風格。這個時候瀏覽器就無法裁決采用哪一種策略了。當然我們可以為瀏覽器引入“排它鎖”或者是“樂觀鎖”來解決這些沖突,但為了避免引入了更大的復雜性,所以JavaScript從誕生開始就選擇了單線程執行。 因為單線程執行,所以對于JavaScript的任務而言,在同一時間內只能執行一個特定的任務,并且它會阻塞其他的任務執行。那么JavaScript的執行不會很慢嗎?特別是對于長時間任務執行的時候,那么其他的任務就得不到執行。然而在軟件開發中,特別是應用軟件開發中,對于I/O設備的訪問都是一些及其耗時的操作。在這些耗時任務執行的時候,其實并沒必要等待它的完成,在I/O任務完成之前JavaScript完全可以繼續執行其他的任務,直到I/O任務完成后再繼續執行該任務的處理就行。JavaScript在設計之初,就意識這一點。所以在JavaScript中將這些耗時的I/O等操作封裝為了異步的方法,等到這些任務完成后就將后續的處理操作封裝為JavaScript任務放入執行任務隊列中,等待JavaScript線程空閑的時候被執行。因此這里形成了另一個話題“瀏覽器的事件循環”機制,將在后續中詳細闡述。 因為在JavaScript語言中,和其他大多數語言不一樣之處:JavaScript中耗時的I/O操作都被處理為異步操作,以及回調注冊機制。異步和回調仿佛和JavaScript就是“與生俱來”的一樣。如Nodejs創始人Ryan Dahl所言,JavaScript語言的非阻塞的異步I/O事件驅動模型,以及JavaScript在Chrome推進下的多次性能優化、具有函數式等高級語言特性,因此最終Nodejs選擇JavaScript。由于Nodejs最終選擇了JavaScript,從此也大大的推動了JavaScript在非瀏覽器領域的急速擴展。 下面的文字是來自Nodejs官網: 當然對于非I/O的操作耗時操作如上篇博文《Promise的前世今生和妙用技巧》所說,在HTML5中也提高了新的解決方案,它就是Web Worker。Web Worker就是在當前JavaScript的執行主線程中利用Worker類新開辟一個額外的線程來加載和運行特定的JavaScript文件,這個新的線程和JavaScript的主線程之間并不會互相影響和阻塞執行的;并且在Web Worker中提供這個新線程和JavaScript主線程之間數據交換的接口:postMessage和onMessage事件。但在HTML5 Web Worker中是不能操作DOM的,任何需要操作DOM的任務都需要委托給JavaScript主線程來執行,所以雖然引入HTML5 WebWorker但仍然沒有改線JavaScript單線程的本質。對于HTML5的Web Worker和在C# WinForm設計中的BackgroundWorker很類似,對于這類GUI(圖形化界面)操作的應用程序中,對于UI界面的操作都需要委托給UI主線程來執行,避免多線程情況下UI操作的安全性和避免不必要的多線程訪問控制的復雜度。 瀏覽器事件循環在上面已經提到JavaScript中為了不阻塞UI的渲染,很多JavaScript任務都是異步的,它們包括鍵盤、鼠標I/O輸入輸出事件、窗口大小的resize事件、定時器(setTimeout、setInterval)事件、Ajax請求網絡I/O回調等。當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去。在瀏覽器內部中存在一個消息循環池,也叫Event Loop(事件循環),JavaScript引擎在運行時后單線程的處理這些事件任務。例如用戶在網頁中點擊了button事件,則它們會被放入在這個事件循環池中,需要等到JavaScript運行時執行線程空閑時候才會按照隊列先進先出的原則被一一執行。對于setTimeout這類定時任務也是一樣的,只有當定時時刻達到的時候,它們才會被放入瀏覽器的事件隊列中等待被執行;由于此時的JavaScript主線程也許并不空閑,所以它將并不會被立即執行,因為在JavaScript語言設計中setTimeout這類定時任務的執行時間并不是精確的。在前端開發中經常會發現setTimeout(func, 0)很有用,因為這并不是理解執行,而是將當前執行回調函數放入瀏覽器的事件隊列中,等待當前其他任務的完成,然后在執行它;所以setTimeout(func, 0)具有改變當前代碼執行順序的作用,讓瀏覽器有機會完成UI界面渲染等任務后在執行這段回調函數。當然對于老式瀏覽器這里具有16ms的差距,HTML5規定為4ms,以及關于動畫操作中的requestAnimationFrame,請讀者參見MDN資料https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame。 瀏覽器事件循環如下圖所示: 雖然JavaScript是單線程執行的,但是瀏覽器并不是單線程執行的,它們有JavaScript的執行線程、UI節點的渲染線程,圖片等資源的加載線程,以及Ajax請求線程等。在Chrome設計中,為了防止因一個Tab window的奔潰而影響整個瀏覽器,它的每一個Tab被設計為一個進程;在Chrome設計中存在很多的進程,并利用進程間通訊來完成它們之間的同步,因此這也是Chrome快速的法寶之一。對于Ajax的請求也需要特殊線程來執行,當需要發送一個Ajax請求的時候,瀏覽器會開辟一個新的線程來執行HTTP的請求,它并不會阻塞JavaScript線程的執行,HTTP請求狀態變更事件會被作為回調放入到瀏覽器的事件隊列中等待被執行。 總結寫到這里,本文也進入了尾聲。希望這篇文章能給閱讀本文的讀者一些啟發,同時如果本文中存在不足的地方,也希望你能不吝指教。另外,同時也歡迎關注博主的微信公眾號[破狼](微信二維碼位于博客右側),這里將會為大家第一時間推送博主的最新博文,謝謝大家的支持和鼓勵。 |
|
來自: 昵稱10504424 > 《工作》