JavaScript多線程編程

遠離瀏覽器卡頓,提升用戶體驗,提高代碼運行效率,使用多線程編程方法。

瀏覽器端JavaScript是以單線程的方式執行的,也就是說JavaScript和UI渲染佔用同一個主線程,那就意味着,若是JavaScript進行高負載的數據處理,UI渲染就頗有可能被阻斷,瀏覽器就會出現卡頓,下降了用戶體驗。javascript

爲此,JavaScript提供了異步操做,好比定時器(setTimeout、setInterval)事件、Ajax請求、I/O回調等。咱們能夠把高負載的任務使用異步處理,它們將會被放入瀏覽器的事件任務隊列(event loop)中去,等到JavaScript運行時執行線程空閒時候,事件隊列纔會按照先進先出的原則被一一執行。html

nodejs引覺得榮的異步處理

經過相似定時器,回調函數等異步編程方式在日常的工做中已經足夠,可是若是作複雜運算,這種方式的不足就逐漸體現出來,好比settimeout拿到的值並不正確,或者頁面有複雜運算的時候很容易觸發假死狀態,異步代碼會影響主線程的代碼執行,異步終究仍是單線程,不能從根本上解決問題。前端

多線程(Web Worker)就應運而生,它是HTML5標準的一部分,這一規範定義了一套 API,容許一段JavaScript程序運行在主線程以外的另一個線程中。將一些任務分配給後者運行。在主線程運行的同時,Worker(子)線程在後臺運行,二者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(一般負責 UI 交互)就會很流暢,不會被阻塞或拖慢。java

什麼是web worker

圖片描述

worker是window對象的一個方法,就是用它來建立多線程。能夠經過如下方式來檢測你的瀏覽器是否支持workernode

if (window.Worker) {…… your code ……}

一個worker是使用一個構造函數(Worker())建立的一個對象,這個構造函數須要傳入一個的JavaScript文件,這個文件包含將在工做線程中運行的代碼。相似於這樣:git

let myWorker = new Worker('worker.js');

主線程和子線程的數據不是共享的,worker經過postMessage() 方法和onmessage事件進行數據通訊。主線程和子線程是雙向的,均可以發送和監聽事件。向一個worker發送消息須要這樣作(main.js):github

myWorker.postMessage('hello, world'); // 發送
worker.onmessage = function (event) { // 接收
    console.log('Received message ' + event.data);
    doSomething();
}

 postMessage所傳的數據都是拷貝傳遞(ArrayBuffer類型除外),因此子線程也是相似傳遞(worker.js)web

addEventListener('message', function (e) {
    postMessage('You said: ' + e.data);
}, false);

 當子線程運行結束後,使用完畢,爲了節省系統資源,能夠手動關閉子線程。若是worker沒有監聽消息,那麼當全部任務執行完畢(包括計數器)後,它就會自動關閉。ajax

// 在主線程中關閉
worker.terminate();
// 在子線程裏線程
close();
Worker也提供了錯誤處理機制,當出錯時會觸發error事件。
// 監聽 error 事件
worker.addEventListener('error', function (e) {
  console.log('ERROR', e);
});

web worker自己很簡單,可是它的限制特別多。chrome

使用的問題

一、同源限制

分配給Worker 線程運行的腳本文件(worker.js),必須與主線程的腳本文件(main.js)同源。這裏的同源限制包括協議、域名和端口,不支持本地地址(file://)。這會帶來一個問題,咱們常用CDN來存儲js文件,主線程的worker.js的域名指的是html文件所在的域,經過new Worker(url)加載的url屬於CDN的域,會帶來跨域的問題,實際開發中咱們不會吧全部的代碼都放在一個文件中讓子線程加載,確定會選擇模塊化開發。經過工具或庫把代碼合併到一個文件中,而後把子線程的代碼生成一個文件url。
解決方法:
(1)將動態生成的腳本轉換成Blob對象。
(2)而後給這個Blob對象建立一個URL。
(3)最後將這個建立好的URL做爲地址傳給Worker的構造函數。

let script = 'console.log("hello world!");'
let workerBlob = new Blob([script], { type: "text/javascript" });
let url = URL.createObjectURL(workerBlob);
let worker = new Worker(url);

二、訪問限制

Worker子線程所在的全局對象,與主線程不在同一個上下文環境,沒法讀取主線程所在網頁的 DOM 對象,也沒法使用document、window、parent這些對象,global對象的指向有變動,window須要改寫成self,不能執行alert()方法和confirm()等方法,只能讀取部分navigator對象內的數據。另外chrome的console.log()卻是可使用,也支持debugger斷點,增長調試的便利性。

三、使用異步

Worker子線程中可使用XMLHttpRequest 對象發出 AJAX 請求,可使用setTimeout() setInterval()方法,也可以使用websocket進行持續連接。也能夠經過importScripts(url)加載另外的腳本文件,可是仍然不能跨域。

應用場景:

一、使用專用線程進行數學運算

Web Worke設計的初衷就是用來作計算耗時任務,大數據的處理,而這種計算放在worker中並不會中斷前臺用戶的操做,避免代碼卡頓帶來沒必要要的用戶體驗。例如處理ajax返回的大批量數據,讀取用戶上傳文件,計算MD5,canvas的位圖的過濾,分析視頻和聲頻文件等。worker中除了缺失了DOM和BOM操做能力之外,仍是擁有很是強大的js邏輯運算處理的能力的,至關於nodejs一個級別的的運行環境。

二、高頻的用戶交互

高頻的用戶交互適用於根據用戶的輸入習慣、歷史記錄以及緩存等信息來協助用戶完成輸入的糾錯、校訂功能等相似場景,用戶頻繁輸入的響應處理一樣能夠考慮放在web worker中執行。例如,咱們能夠 作一個像Word同樣的應用:當用戶打字時,後臺當即在詞典中進行查找,幫助用戶自動糾錯等等。

三、數據的預取

對於一些有大量數據的先後臺交互產品,能夠新開一個線程專門用來進行數據的預取和緩衝數據,worker能夠用在本地web數據庫的行寫入和更改,長時間持續的運行,不會被主線程上的活動(好比用戶點擊按鈕、提交表單)打斷,也有利於隨時響應主線程的通訊。也能夠配合XMLHttpRequest和websocket進行不斷開的通訊,實現守衛進程。

兼容性

圖片描述

整體來講,兼容性仍是不錯的, 移動端能夠放心使用,桌面端要求不高的話,也可使用。

superWorker

爲了更方便快捷的使用web worker,咱們封裝了一個工具,能夠經過模塊化的方式編寫運行在web worker中的腳本,避免同源策略,減小服務端發送一個額外的url請求,無需瞭解web worker,就像使用setTimeout同樣,快速使用superWorker,提高你的編碼效率和運行效率,它有如下優勢:
一、原生JS實現,無任何依賴庫。
二、簡單快速,擯棄繁瑣的建立文件、綁定事件,實現無侵入、無感知運行新線程的代碼。
三、返回Promise類型的數據,支持鏈式調用,清晰明瞭。
四、支持多種方式新建worker,包括匿名函數、函數列表、文本文件、html片斷、url、類,方便快捷。
五、gzipped壓縮後僅僅 1.2kb。

使用教程:

import superWorker from 'superWorker'
let worker = superWorker(function (a, b) {
    // 子線程中要運行的代碼
    return a + b;
});
worker.start(1, 2).then((r)=>console.log(r)); // 3

用法

superWorker(code, [type])

參數

code:運行的代碼, type(非必須):代碼類型,目前支持0、一、二、三、4。

實現原理:

先進行源代碼轉文件:

let workerBlob = new Blob(code, { type: "text/javascript" });
let url = URL.createObjectURL(workerBlob);

對類型拆分,code參數支持傳入匿名函數、函數列表、文本文件、url、HTML內嵌標籤、類等功能,首先對傳入的代碼進行分類匹配,字符串化,而後進行拼接運行

code = `(${Function.prototype.toString.call(code)})(${exportsObjName})`;

 對於傳入的方法,分別在主線程中的exports對象進行標記,和worker子線程中的exportsObjName對象中進行賦值。對於ES6 模塊化的代碼,進行過濾轉譯。

// 處理 \nexport default function xxx(){}  => exports.default = true; exportsObjName.default = function xx(){}
code = code.replace(/^(\s*)export\s+default\s+/m, (s, before) => {
    exports.default = true;
    return `${before}${exportsObjName}.default=`;
});

 造成主線程exports和子線程exportsObjName中的方法進行一一對應。

worker主線程與主線程進行通信則是仍然須要經過postMessage方法和onmessage回調事件來進行,這個咱們統一進行了雙向綁定,分別對主線程和子線程執行setup。

function setup(ctx, pmMethods, callbacks) {
    ctx.addEventListener('message', ({ data }) => {
    // ……
    })
}

 在主線程中對worker封裝了一些快捷的方法,好比關閉線程:

worker.terminate = () => {
    URL.revokeObjectURL(url);
    term.call(this);
};

 並把子線程擁有的方法、屬性,暴露出來,方便主線程經過傳遞參數調用。

worker.expose = methodName => {
    worker[i] = function () {
        return worker['call'](methodName, [].slice.call(arguments));
    };
};

 大體以下圖:
圖片描述

歡迎小夥伴們使用以及批評指正。有問題多多反饋,多多交流。

小結

對於web worker這項新技術,不管在PC仍是在移動web,都很實用,騰訊新聞前端組進行了普遍的嘗試,Web Worker 的實現爲前端程序帶來了後臺計算的能力,實現了主 UI 線程與複雜計運算線程的分離,從而極大減輕了因計算量大而形成 UI 阻塞而出現的界面渲染卡、掉幀的狀況,而且更大程度地利用了終端硬件的性能。superWorker能解決掉事件綁定,同源策略等繁瑣的問題,它目前最大的問題在於不兼容IE9,在兼容性要求不是那麼嚴格的地方,儘量的使用吧!

最後,TNFE團隊爲前端開發人員整理出了小程序以及web前端技術領域的最新優質內容,每週更新✨,歡迎star,github地址:https://github.com/Tnfe/TNFE-Weekly

做者:TNFE 大鵬哥

加入TNFE前端溝通羣

相關文章
相關標籤/搜索