瀏覽器的多線程和單線程
學習過 JavaScript 的可能會了解,JavaScript 的宿主瀏覽器只有一個線程運行 JavaScript,除了 JavaScript 的線程,瀏覽器中單個頁面還有一些其他線程,例如:UI 線程負責處理渲染 DOM 元素;GUI 線程用于處理與用戶交互的邏輯;網絡線程用于發送接收 HTTP 請求;file 線程用于讀取文件;定時器線程處理定時任務等等。
1. 單線程原因
為什么不能像很多高級語言一樣支持多線程呢?假定 JavaScript 同時有兩個線程,一個線程在HTML中創建了一個標簽元素,另一個線程刪除了這個標簽,這時瀏覽器應該執行什么操作?瀏覽器中 JavaScript 的主要用途是操作 DOM 。這決定了它只能是單線程,否則會帶來很復雜的同步問題。為了避免復雜性,大部分主流瀏覽器的 JavaScript 運行環境只支持單線程。
2. JavaScript 的事件驅動
既然 JavaScript 只支持單線程,那么有人可能會好奇為什么瀏覽器中的 JavaScript 可以同時發送多個網絡請求或者執行多個事件回調函數呢?
這是因為 JavaScript 是基于事件驅動,當需要進行網絡請求時,JavaScript 線程會把請求發送給 network 線程執行,并等待執行結果;當進行文件讀取時則調用 file 線程,然后等待結果。然后 JavaScript 會一直輪詢事件庫 event loop,直到有事件完成,這時瀏覽器會驅動 JavaScript 去執行事件的回調函數。這就是 JavaScript 的事件驅動模型。

3. web worker誕生
單線程的最大問題是不能利用多核 CPU 的優點,HTML5 推出的 Web Worker 標準,允許 JavaScript 創建多線程,但是子線程受主線程約束,且不得操作 DOM 。所以,這個新標準不會產生多線程同步的問題。
4. 適用場景
Web Worker 能解決傳統的 JavaScript 單線程出現的執行阻塞問題,因而適合以下幾種業務場景:
- 并行計算;
- ajax 輪詢;
- 耗時的函數執行;
- 數據預處理/加載。
5. 函數介紹
5.1 創建
初始化一個 Web Worker,由于不是所有的瀏覽器都支持 Web Worker,所以需要判斷一下瀏覽器是否支持:
if (window.Worker) {//判斷瀏覽器是否支持web worker
var worker = new Worker('test.js');//創建一個線程,參數為需要執行的JavaScript文件
}
5.2 向線程傳遞參數
新的線程的上下文環境跟原宿主環境相對獨立的,所以變量作用域不同,如果需要互相讀取變量的話需要通過消息發送的方式傳輸變量,例如:
worker.postMessage('test'); //數據類型可以是字符串
worker.postMessage({method: 'echo', args: ['Work']});//數據類型可以是對象
5.3 主線程接受消息
跟上述場景類似,主線程也需要通過監聽的方式獲取輔線程的消息:
worker.onmessage = function (event) {
console.('接收到消息: ' + event.data);
}
5.4 線程加載腳本
子線程內部也可以通過函數加載其他腳本:
5.5 關閉線程
6. 使用 JavaScript 多線程實現非阻塞全排列
6.1 什么是全排列
從 n 個不同元素中任取 m(m≤n)個元素,按照一定的順序排列起來,叫做從 n 個不同元素中取出 m 個元素的一個排列。當 m=n 時所有的排列情況叫全排列。
6.2 為什么使用多線程處理
這里并非突出使用 JavaScript 實現全排列的優勢,而是在實際項目中類似這種科學運算相關的算法可能會消耗一定的 CPU,由于 JavaScript 是解釋型語言,運算性能是它的弱項,而且瀏覽器中運行的 JavaScript 又是單線程的,所以一旦出現性能問題可能會導致線程阻塞,阻塞之后會導致頁面卡頓,非常影響用戶體驗。使用 webworker 的多線程功能將這個運算函數單獨 fork 出一個子線程去運行,運行完成之后發送結果給主線程,可以有效的避免性能問題。
6.3 代碼示例
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=UTF-8">
<title>JavaScript實現全排列</title>
<script type="text/JavaScript">
function combine() {//點擊按鈕向webworker線程發送請求
var worker = new Worker('http://wiki-code.oss-cn-beijing.aliyuncs.com/html5/js/worker.js');
worker.postMessage(document.getElementById("str").value);
worker.onmessage= function (event) {
document.getElementById("result").innerHTML = event.data ; //監聽JavaScript線程的結果
};
}
</script>
</head>
<body>
<input type="text" id="str" />
<button onclick="combine()">全排列</button>
結果是:<div id="result" style="width:500px;height:500px;word-break: break-all;"></div>
</body>
</html>
worker.js 代碼如下:
function getGroup(data, index = 0, group = []) {//生成全排列
var need_apply = new Array();
need_apply.push(data[index]);
for(var i = 0; i < group.length; i++) {
need_apply.push(group[i] + data[index]);
}
group.push.apply(group, need_apply);
if(index + 1 >= data.length) return group;
else return getGroup(data, index + 1, group);
}
onmessage = function(message){//監聽主線程的數據請求
var msg = message.data;
if(msg == "") postMessage("請輸入正確的字符串");
else {
var data = msg.split("");//將字符串轉數組
postMessage(getGroup(data));
}
}
上述代碼實現了一個使用 JavaScript 的 Web Worker 實現的全排列的功能。上半部分是主線程的代碼,主要實現了創建子線程、發送數據給子線程、接收子線程的消息這幾個功能;下半部分是子線程,子線程主要負責運算,并將運算結果發送給主線程。
7. 總結
早期的 JavaScript 由于考慮操作 DOM 的一致性問題,以及當時的網頁沒有過多的交互所以不需要大量的計算,所以只支持單線程。這在多核 CPU 時代的劣勢愈發明顯,所以 HTML5 中推出多線程解決這個問題?;仡櫛菊轮饕榻B了 Web Worker 的使用方式以及其適用場景。