知道html5 Web Worker標準嗎?能實現JavaScript的多線程?

js爲什麼是單線程?

主要是因爲最開始javascript是單純的服務於瀏覽器的一種腳步語言(那時候沒有nodejs)。瀏覽器是爲了渲染網頁,通過dom與用戶交互,如果一個線程需要給dom執行click事件,而另一個進程要刪除這個dom,這2個動作可能同時進行,也可能先後進行(像java,c#等語言中會引入鎖的概念,這樣會變得異常複雜),那麼就會造成很多不可預料的錯誤。

所以,爲了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵。爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質。

瀏覽器是多線程的

瀏覽器打開一個tab,就會單獨開一個進程,這個進程包含多個線程,參考:JS運行機制
主要包含的線程有:

  1. GUI渲染線程

負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行
注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行。

  1. JS引擎線程

也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負責解析Javascript腳本,運行代碼。
JS引擎一直等待着任務隊列中任務的到來,然後加以處理,一個Tab頁(renderer進程)中無論什麼時候都只有一個JS線程在運行JS程序
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。

  1. 事件觸發線程

歸屬於瀏覽器而不是JS引擎,用來控制事件循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
注意,由於JS的單線程關係,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

  1. 定時觸發器線程

傳說中的setIntervalsetTimeout所在線程
瀏覽器定時計數器並不是由JavaScript引擎計數的,(因爲JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確)
因此通過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。

  1. 異步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 對象,也無法使用documentwindowparent這些對象。但是,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 使用教程

兼容性

1.png
可以看到除了Opera Mini瀏覽器,連IE都能使用了,所以兼容性問題不大。

總結

  1. 由於javaScript的最初設計特點,採用了單線程的運行機制。
  2. 瀏覽器是多個線程相互協作來工作的,但是GUI渲染線程與JS引擎線程互斥
  3. js線程在運行時,會鎖死GUI渲染線程,爲了利用多核CPU的計算能力,HTML5提出Web Worker標準。
  4. Web Worker的使用有一些限制,比如說:同源限制,DOM限制,文件限制等,但能解決在js需要大量計算工作時,頁面卡頓的問題。
  5. Web Worker實際上是js線程的一個子線程,理論上js還是單線程的。

學習如逆水行舟,不進則退,前端技術飛速發展,如果每天不堅持學習,就會跟不上,我會陪着大家,每天堅持推送博文,跟大家一同進步,希望大家能關注我,第一時間收到最新文章。

個人公衆號:長按保存關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章