1. 程式人生 > >當非同步不再能滿足需求:對瀏覽器中的多執行緒的介紹

當非同步不再能滿足需求:對瀏覽器中的多執行緒的介紹

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

原文轉載自 https://neoteric.eu/blog/when-async-is-not-enough-introduction-to-multithreading-in-the-browser/

先說最重要的:JavaScript程式碼可以非同步執行,但並不意味著它是跑在多個執行緒裡。那麼非同步到底是什麼意思?讓我們想象發一個Ajax請求,向服務端請求資料。你並不是立即得到響應——你需要等待一小段時間,讓服務端返回資料。在等待響應的過程中,程式執行著你其他部分的程式碼。如果不是這樣,Ajax請求會凍結住,不讓後面的程式碼執行,直到收到服務端的響應——這不是我們想要的,對吧?

事件迴圈(Event Loop)

在JavaScript執行環境中,有個非常重要的概念,叫事件迴圈。它周而復始地工作著,每一次迴圈被稱為一個"tick"。如果在某一個tick中,有等待著的事件佇列需要處理,那麼它們會一個個地被執行。大家所熟知的setTimeout函式就是一個很好的例子。它的第一個引數是一個回撥函式——一個在某段時間之後被執行的函式。當setTimeout被解析時,它被壓入函式呼叫棧的棧頂,它設定一個定時器,然後就從棧頂彈出,把你的回撥函式塞到事件迴圈的後面——那意味著這個回撥函式不會精確地在定義的時間間隔後執行——在事件佇列中等待的其他事件需要被優先處理。當時機到來,你的回撥函式被壓入函式呼叫棧的棧頂,然後執行。你發向伺服器的請求,也是同樣的原理——你定義一個回撥函式,當收到響應後,它被塞進事件迴圈佇列的後面。

函式呼叫棧(Call Stack)

函式呼叫棧是一個底層的資料結構——它記錄我們執行到程式哪兒了。當程式進入一個函式,就把它放在棧頂,當從函式中返回,就意味著把它從棧中彈出。讓我們使用一點遞迴式的邏輯來簡單展示一下:

function factorial(n) {
  if(n === 1 || n === 0) {
    return 1;
  }
  return factorial(n - 1) * n;
}
console.log(factorial(3)); // 3! = 6

WebWorkers

你已經看到,非同步程式碼,解決的是一件事情"現在發生"還是"以後發生",而不是解決如何讓"多個事情同時發生"。但如果有一些處理器密集型任務,我們擔心它會讓介面卡住,怎麼辦?

答案是WebWorkers。它允許JavaScript程式碼在後臺以一個獨立的執行緒被執行。它允許主執行緒流暢執行,不被阻塞。WebWorkers在另一個與window不同的全域性上下文環境中。這也帶來了一些侷限:比如,你不能直接在Worker裡操作DOM。最基礎的(也是瀏覽器支援得最好的)WebWorker型別是Dedicated Worker。

想建立一個Worker,你需要向Worker建構函式傳入一個檔名,在該檔案中包含了需要執行的JavaScript指令碼。

// 在主執行緒
var factorialWorker = new Worker('factorial.worker.js');

比如說,我們想得到一整組數字的階乘。

想向Worker傳資料,你需要呼叫postMessage方法:

// 在主執行緒
var arr = [50, 100, 125, 150];
for(var i = 0; i < arr.length; ++i) {
  factorialWorker.postMessage(arr[i]);
}

你可以通過事件在主執行緒和Worker執行緒之間通訊。如果你想監聽Worker的返回值,就在主執行緒註冊一個事件監聽器。

// 在主執行緒
factorialWorker.addEventListener('message', function(event) {
  console.log('!' + event.data.number + ' = ' + event.data.factorial);
});

這會輸出傳入給Worker的數字的階乘。

剩下唯一要做的事情就是建立factorial.workder.js檔案。

它需要返回當前計算的數字的階乘,還要定義計算階乘的函式本身。

在Worker中,有一個self屬性。它返回指向WorkerGlobalScope的引用。利用它,我們可以和向Worker傳送資料的指令碼通訊。  

// factorial.workder.js
function factorial(n) {
  if(n === 1 || n === 0) {
    return 1;
  }
  return factorial(n - 1) * n;
}

self.addEventListener('message', function(event) {
  self.postMessage({ number: event.data, factorial: factorial(event.data) });
});

這裡發生的情況是,我們建立了一個新的Worker,並監聽它給我們返回的資料。然後,我們向它傳送資料——Worker會得到資料,在完成它內部的計算之後,向我們傳送一個響應。所有的計算都在一個單獨執行緒中完成。很酷吧?

不過你可能會遇到一些問題。第一個問題是Chrome不能以本地檔案的方式使用WebWorkers。不過你可以開啟一個http伺服器來嘗試使用它。

Webpack

另一個問題可能在你使用Webpack時出現。它可能會給你一個404 Not Found錯誤,因為它不知道你想以WebWorker的形式載入檔案。你需要額外的載入器(loader)來載入類似的檔案。讓我帶你看看這個過程。首先,用npm安裝載入器:

npm install --save-dev worker-loader

然後你需要在webpack.config.js中新增一條規則:

module: {
  rules: [
    {
      test: /\.worker\.js$/,
      use: { loader: 'worker-loader' }
        
    },
    (...)
	]
}

現在,如果你引入以.workder.js結尾的檔案,Webpack會使用worker-loader來載入。讓我們用ES6的一些特性來修改一下程式碼:  

import FactorialWorker from './factorial.worker.js';

const factorialWorker = new FactorialWorker();
factorialWorker.addEventListener('message', event => {
  console.log(`!${event.data.number} = ${event.data.factorial}`);
});

const arrayOfNumbers = [50, 100, 125, 150];
for(let number of arrayOfNumbers) {
  factorialWorker.postMessage(number);
}

總結一下,當開發一個背後有很多操作(尤其是密集型計算)的富應用時,WebWorkers會非常有幫助。嘗試一下,親自看看吧。我鼓勵你去試驗。