js為什么是單線程?
主要是因為最開始javascript
是單純的服務于瀏覽器的一種腳步語言(那時候沒有nodejs
)。瀏覽器是為了渲染網頁,通過dom
與用戶交互,如果一個線程需要給dom
執行click
事件,而另一個進程要刪除這個dom
,這2個動作可能同時進行,也可能先后進行(像java,c#
等語言中會引入鎖的概念,這樣會變得異常復雜),那么就會造成很多不可預料的錯誤。
所以,為了避免復雜性,從一誕生,
JavaScript
就是單線程,這已經成了這門語言的核心特征。為了利用多核CPU的計算能力,HTML5
提出Web Worker標準,允許JavaScript
腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM
。所以,這個新標準并沒有改變JavaScript
單線程的本質。
瀏覽器是多線程的
瀏覽器打開一個tab,就會單獨開一個進程,這個進程包含多個線程,參考:JS運行機制
主要包含的線程有:
- GUI渲染線程
負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,布局和繪制等。
當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行
注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當于被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。
- JS引擎線程
也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負責解析Javascript腳本,運行代碼。
JS引擎一直等待著任務隊列中任務的到來,然后加以處理,一個Tab頁(renderer進程)中無論什么時候都只有一個JS線程在運行JS程序
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。
- 事件觸發線程
歸屬于瀏覽器而不是JS引擎,用來控制事件循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
注意,由于JS的單線程關系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行)
- 定時觸發器線程
傳說中的
setInterval
與setTimeout
所在線程
瀏覽器定時計數器并不是由JavaScript引擎計數的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確)
因此通過單獨線程來計時并觸發定時(計時完畢后,添加到事件隊列中,等待JS引擎空閑后執行)
注意,W3C在HTML標準中規定,規定要求setTimeout中低于4ms的時間間隔算為4ms。
- 異步http請求線程
在
XMLHttpRequest
在連接后是通過瀏覽器新開一個線程請求
將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由JavaScript
引擎執行。
上面列出的線程之間,有一個重要的規則是:GUI渲染線程與JS引擎線程互斥,那么我們可以得出以下結論JS阻塞頁面加載,那么在js
運行的這段時間內,GUI
的渲染會停止,這段時間內的界面交互,DOM
的重繪與回流會停止,會被保存到待執行隊列中,直到js
線程空閑,才會執行這些隊列。
我們用下面的一段代碼和運行結果來說明這個機制:
<html>
<head>
<style>
.box {
width: 200px;
height: 200px;
margin-top: 100px;
background: #f09;
animation: bounce 2s linear 0s infinite alternate;
background-image: linear-gradient(45deg, #3023AE 0%, #f09 100%);
}
@keyframes bounce {
0% {
border-radius: 40% 60% 72% 28% / 70% 77% 23% 30%;
}
100% {
border-radius: 75% 25% 24% 76% / 13% 15% 85% 87%;
}
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
// 計算斐波那契數列,這個數列從第3項開始,每一項都等于前兩項之和。
function recurFib(n) {
if (n < 2) {
return n;
} else {
return recurFib(n - 1) + recurFib(n - 2)
}
}
window.onload = function () {
setTimeout(function () {
console.time("運算耗時:")
// 計算n為40的結果
console.log('結果:', recurFib(40))
console.timeEnd("運算耗時:")
}, 2000)
document.getElementsByClassName("box")[0].addEventListener('click', function () {
console.log('click')
})
}
</script>
</html>
可以看到,一開始網頁和動畫正常運行,但是開始執行計算斐波那契數列后,動畫就停止了,頁面也停止響應鼠標的click
事件了,直到recurFib(40)
計算出結果后,動畫才開始繼續執行,而期間積攢的click
事件也在一起被執行。這就解釋了GUI渲染線程與JS引擎線程互斥。由于這個弊端HTML5
提出Web Worker標準。
利用Web Worker開啟一個子線程
Web Worker 有以下幾個使用注意點。
1.同源限制
分配給 Worker 線程運行的腳本文件,必須與主線程的腳本文件同源。
2.DOM 限制
Worker 線程所在的全局對象,與主線程不一樣,無法讀取主線程所在網頁的 DOM 對象,也無法使用document
、window
、parent
這些對象。但是,Worker 線程可以navigator
對象和location
對象。
3.通信聯系
Worker 線程和主線程不在同一個上下文環境,它們不能直接通信,必須通過消息完成。
4.腳本限制
Worker 線程不能執行alert()
方法和confirm()
方法,但可以使用 XMLHttpRequest 對象發出 AJAX 請求。
5.文件限制
Worker 線程無法讀取本地文件,即不能打開本機的文件系統(file:
),它所加載的腳本,必須來自網絡。
以上規則引用阮一峰老師的: Web Worker 使用教程
創建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)
JS引擎線程與worker線程間通過特定的方式通信(postMessage API
,需要通過序列化對象來與線程交互特定的數據)。
下面我們用worker
的相關api
來解決上面卡頓的問題。
<!--index.html主線程-->
<html>
<head>
<style>
.box {
width: 200px;
height: 200px;
margin-top: 100px;
background: #f09;
animation: bounce 2s linear 0s infinite alternate;
background-image: linear-gradient(45deg, #3023AE 0%, #f09 100%);
}
@keyframes bounce {
0% {
border-radius: 40% 60% 72% 28% / 70% 77% 23% 30%;
}
100% {
border-radius: 75% 25% 24% 76% / 13% 15% 85% 87%;
}
}
</style>
</head>
<body>
<div class="box"></div>
</body>
<script>
window.onload = function () {
// 創建一個子線程worker實例
var worker = new Worker('./test.js');
setTimeout(function () {
// 通信:向子線程發送消息
worker.postMessage('start')
}, 2000)
worker.addEventListener('message', function(res) {
// 通信:收到子線程消息
console.log('result:',JSON.stringify(res.data));
// 關閉worker線程
worker.terminate();
})
document.getElementsByClassName("box")[0].addEventListener('click', function () {
console.log('click')
})
}
</script>
</html>
// test.js子線程代碼
// 通過監聽message來接受主線程中的消息
addEventListener('message', function(res) {
// 子線程向主線程中發生消息
// 計算斐波那契數列,這個數列從第3項開始,每一項都等于前兩項之和。
if(res.data === 'start') {
// 開始運算
console.log('收到主線程消息,開始運算')
function recurFib(n) {
if(n < 2){
// 主動關閉子線程
// this.close()
return n ;
}else {
return recurFib(n-1)+recurFib(n-2)
}
}
console.time("運算時間:")
// 計算n為40的結果
var count = recurFib(40)
console.timeEnd("運算時間:")
// 向主線程發送消息
console.log('運算完畢,發送消息給主線程!')
this.postMessage(count);
}
})
運行結果:
可以看到整個運行過程動畫沒有卡頓,也能響應click
事件,所以在我們遇到大型計算的時候,請單獨開啟一個worker
子線程來解決js
線程阻塞GUI
線程的問題。上文中只涉及到一部分worker API
。關于worker
更詳細更具體的用法可以參見: Web Worker 使用教程
兼容性
可以看到除了Opera Mini瀏覽器,連IE都能使用了,所以兼容性問題不大。
總結
- 由于
javaScript
的最初設計特點,采用了單線程的運行機制。 - 瀏覽器是多個線程相互協作來工作的,但是GUI渲染線程與JS引擎線程互斥。
-
js
線程在運行時,會鎖死GUI
渲染線程,為了利用多核CPU的計算能力,HTML5
提出Web Worker標準。 -
Web Worker
的使用有一些限制,比如說:同源限制,DOM
限制,文件限制等,但能解決在js
需要大量計算工作時,頁面卡頓的問題。 -
Web Worker
實際上是js
線程的一個子線程,理論上js
還是單線程的。
學習如逆水行舟,不進則退,前端技術飛速發展,如果每天不堅持學習,就會跟不上,我會陪著大家,每天堅持推送博文,跟大家一同進步,希望大家能關注我,第一時間收到最新文章。