什麼是Web Worker
15年前,也就是2008年,html
第五版html5
釋出,這一版的釋出,提供了不少新的好用的功能,如:
- Canvas繪圖
- 拖拽drag
- websocket
- Geolocation
- webworker
- 等...
筆者之前說過:一項新技術新的技術方案的提出,一定是為了解決某個問題的,或者是對某種方案的最佳化
那麼Web Worker
這個新技術解決了什麼問題?有哪些最佳化價值呢?
讓我們繼續往下看...
官方定義
Web Worker 為 Web 內容在後臺執行緒中執行指令碼提供了一種簡單的方法。執行緒可以執行任務而不干擾使用者介面。此外,他們可以使用XMLHttpRequest執行 I/O (儘管responseXML和channel屬性總是為空)。一旦建立,一個 worker 可以將訊息傳送到建立它的 JavaScript 程式碼,透過將訊息釋出到該程式碼指定的事件處理程式(反之亦然)......
官方MDN地址:https://developer.mozilla.org...
乍一看這段話,好像懂了(Web Worker和執行緒
有關),又好像沒懂。但是我們能看到一個加粗的關鍵字:執行緒。
那麼,新的問題來了,什麼是執行緒?
當我們去學習一個知識點A
的時候,我們會發現知識點A是由a1、a2、a3、a4
組合而成的,所以我們要繼續下鑽,去搞懂a1、a2、a3、a4
分別是什麼意思。這樣才能更好的理解知識點A
的含義內容。
什麼是執行緒
執行緒
基本八股文定義不贅述,大家可以將執行緒
理解成為一個打工仔,每天的工作就是解析翻譯並執行程式碼
,像搬磚一樣,不過一次只能搬一塊磚,後面有磚,不好意思,你等會兒,等我把這個磚搬完了再搬你們。
執行緒道人朗聲道:爾等磚頭列隊稍後,待老夫一塊塊搬!(js中的任務佇列
)
js
是單執行緒語言
,生來便是!java
平常寫程式碼也是單執行緒的操作
,不過單執行緒
有時候不太夠用,於是java
中也提供了多執行緒
的操作入口,比如:Thread類
,Runnable介面
,Callable介面
;大多數語言都是類似的,python
也是的啊,python
寫程式碼平常也是單線
程,另也提供了threading模組
讓程式設計師可以去做多執行緒操作
。
為何js要設計成單執行緒呢?
- 符合大致趨勢
- 這與
js
的工作內容有關:js
只是用來去做一些使用者互動,並呈現效果內容。 - 試想,如果js是多執行緒,執行緒一將
dom元素
的背景色改成紅色,執行緒二將dom元素
的背景色改為綠色,那麼,到底上紅色還是綠色呢? - 不過後來人們發現,某些情況下,
前端如果能做多執行緒的操作
,將會大大提升開發效率 - 於是
Web Worker
就應運而生了 Web Worker
可以建立另外的執行緒去做一些操作,操作不影響js
主執行緒(比如UI渲染
)- 本來只有一個
js主執行緒
搬磚,現在又過來好幾個js輔助執行緒
一塊幫忙搬磚,那肯定效率高啊!
有點道友問,那如果主執行緒操作dom,然後在Web worker建立的輔助執行緒中也去操作dom呢?最終結果聽誰的啊???回答:Web work建立的執行緒中沒有document全域性文件物件,所以無法操作dom,另外,也不會這樣做的
- 大家這樣理解:在實際開發場景中
Web Worker
主要,多數,應用在前端一些複雜中運算 - 而大家都知道一些複雜的運算,基本上交由後端去處理(實際上覆雜的運算操作,後端也會看情況去開啟多執行緒操作的)
- 這樣說來,
Web Worker
的作用就是把後端進行的多執行緒操作運算拿到前端了
- 工作中一些資料的加工、計算、組裝邏輯操作,常常是由後端來幹;但是在某些情況下,如果前端去做的話,效率或許更高一些
所以Web Worker
這個新技術的價值是什麼呢?
Web worker建立輔助執行緒、幫助前端主執行緒進行復雜耗時的計算
一個人手不夠用,那就多搖幾個人。
Web worker建立的一些輔助執行緒,分別去幫主執行緒分擔一些複雜的、耗時的js運算,這樣的話,主執行緒後續的程式碼執行就不會阻塞,當輔助執行緒計算出複雜耗時運算結果後,再與主執行緒通訊,將計算出的結果告知主執行緒。
Web Worker
新技術價值,簡而言之:提升前端程式碼運算執行效率
關於Web worker
的原生語法,諸如:
// 建立一個Web worker
var myWorker = new Worker('worker.js');
// 使用Web worker傳送訊息
worker.postMessage(params)
// 使用Web worker接收訊息
worker.onmessage = function(e) {
console.log(e.data)
}
// 等等...
Web worker
的原生語法,筆者不贅述了,大家可自行去MDN
上的Web Worker去檢視,
為什麼不說呢?因為我們工作中開發程式碼,基本上都是使用框架,在框架中直接寫原生的Web worker
有許多的邊界異常或者其他的情況需要控制。以vue
框架為例,我們不能直接寫Web Worker
,需要使用Webpack
中的worker-loader
外掛去解析Web worker
,並且在vue.config.js
中去做相應配置。
嗯,有些麻煩。
在這個情況下,基於Web Worker
進行封裝的外掛庫vue-worker
,就閃亮登場了。
簡單、好用、便於理解
這裡不提Web worker的原生基本語法不是說大家不用看了,看還是要看的,只不過篇幅原因(懶),這裡就不贅述了。
Web Worker的應用場景
如果大家只是寫增刪改查的業務程式碼,Web Worker用的的確非常少
工作中那些需要進行復雜耗時的計算的邏輯程式碼,都可以由前端使用Web Worker
進行計算。但是複雜耗時的計算基本上都是交由後端進行。這個普遍認知下導致其使用的比較少。
大概有以下場景:
- 加密解密資料(加密解密筆者之前接觸的是
NodeRSA、md5、crypto
) - 提前獲取資料,比如提前傳送
ajax
請求獲取介面的一些圖片、數值等相關資料 - 將耗時運算交由
Web Work
處理(筆者之前做的就是這個)
不過使用的是vue-worker
外掛(基於Web Worker
封裝的外掛)
vue-worker(基於Web worker封裝的一款不錯的外掛)
當我們找到一款還不錯的外掛的時候,我們首先去npm
官網上看看這個外掛的下載量如何,以及GitHub
的star
數量多少。
npm
地址:https://www.npmjs.com/package...
vue-worker
下載量截圖:
好少啊,一共也才不到3000次
下載量,這裡的下載量少是因為,應用場景不多,所以大家使用的也是少,接下來談談自己曾經使用Web worker
的這個場景
曾經應用場景
筆者之前做一個專案,頁面上有很多的輸入框,裡面的輸入框中需要填寫硬體採集的:壓力、溫度、比熱容、質量大小等很多引數,當點選計算按鈕時,會把這些引數進行計算,得出的結果,以供工作人員進行工作參考。
相當於一個計算器
,只不過計算時間會很長
本來說是把這個計算過程交給後端計算,只不過當時後端的同事有事情,請假一週。但是工作不能停下啊
於是筆者就想能不能前端計算呢?
經過一系列調研(谷歌一下),找到了vue-worker
外掛
最終是由筆者這個前端去做的這個計算,時間挺好,產品喜笑顏開
使用步驟
1.下載依賴包
cnpm i vue-worker --save
2.在入口檔案中引入
import Vue from 'vue'
import VueWorker from 'vue-worker' // Web worker外掛
Vue.use(VueWorker)
3.使用
接下來,筆者舉兩個例子,更加便於大家理解
案例一 主執行緒渲染dom、輔助執行緒進行計算
需求:
- 點選按鈕進行計算,計算執行兩個
UI操作
- 第一個
UI操作
,計算斐波那契數fib(44)
的值,且統計計算所用的時長,並寫入到頁面上 - 第二個
UI操作
,每隔0.1秒
,更新頁面上h2標籤
的內容值 - 要求兩個操作
不阻塞
,不能出現後一個UI操作
要等待前一個UI操作
的情況 - 因為
斐波那契是遞迴執行
,是一個比較耗時的操作fib(44)
- 那就想辦法不讓這個耗時的操作堵塞住任務佇列
我們使用vue-worker
提供的方法:this.$worker.run(func, [args]?)
寫法如下:
html
<h1>開啟一個執行緒運算用$worker.run方法</h1>
<br />
<el-button
@click="openOneThread"
type="success"
plain
size="small"
style="margin-bottom: 16px"
>計算斐波那契數列值和用時,以及渲染頁面兩個任務</el-button
>
<div>
斐波那契值為:<span class="bold">{{ fibRes }}</span>
<i v-show="btnLoading" class="el-icon-loading"></i>
執行用時:
<i v-show="btnLoading" class="el-icon-loading"></i>
<span class="bold">{{ fibTime }}</span>
毫秒
</div>
js
// data
worker: null,
btnLoading: false,
fibNum: 44,
fibRes: null, // 斐波那契計算的結果
fibTime: null, // 斐波那契計算用時
// methods
openOneThread() {
/* 第一個UI操作 */
this.btnLoading = true;
this.worker = this.$worker // this.$worker.run(func, [args]?) 一次性的,自動銷燬
.run(
(n) => {
// 注意這裡的函式是內部另一個執行緒空間的,不能從外部引入過來(記憶體空間隔離)
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
let start = Date.now(); // console.time()和console.timeEnd()直接拿不到值,就換種方式
let res = fib(n);
let end = Date.now(); // window.performance.now()也不能用,因為沒window物件
return [res, end - start]; // 返回陣列,第一項是fib(44)的值,第二項是fib(44)遞迴計算用時
},
[this.fibNum] // 引數,從這裡傳遞進去,陣列形式
)
.then((res) => {
console.log("then", res); // 另一個執行緒執行完以後就能拿到結果了
this.fibRes = res[0];
this.fibTime = res[1];
this.btnLoading = false;
})
.catch((err) => {
console.log("err", err);
this.btnLoading = false;
});
/* 第二個UI操作 */
let h2Dom = this.$refs.renderH2;
let n = 0;
let timer = setInterval(() => {
if (n >= 60) {
clearInterval(timer);
} else {
n++;
h2Dom.innerHTML = n;
}
}, 100);
// 使用web worker外掛vue-work的確可以做到不阻塞
},
效果圖
案例二 開啟三個輔助執行緒進行計算看時間
案例一,已經可以看到Web Worker
的優勢
了。接下來,我們再舉個例子。
需求:需要計算3個fib(30)
,如果我們使用Promise
,寫法是這樣的:
Promise.all進行計算
async usePromiseFn() {
function asyncOne() {
let async1 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async1;
}
function asyncTwo() {
let async2 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async2;
}
function asyncThree() {
let async3 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async3;
}
console.time("使用Promise搭配aysnc和await");
// let paramsArr = [asyncOne()]; // 計算一個大概在3秒左右(計算一次重新整理一次頁面,精確一些)
let paramsArr = [asyncOne(), asyncTwo(), asyncThree()]; // 計算三個耗時任務大概在9秒左右
let res = await Promise.all(paramsArr);
console.timeEnd("使用Promise搭配aysnc和await");
console.log("結果", res);
},
},
使用Promise.all
方法去計算3個fib值
,用時在9秒左右
,的確是有些慢。那麼,我們使用Web Worker
方式呢?
此外,使用Promise.all
控制檯也會有警告提醒:[Violation] 'click' handler took 8822ms
意思是:click
事件中執行的程式耗時過長
,看到沒,如果使用js
的主執行緒
去進行復雜計算,瀏覽器都看不下去了...
再一個,大家在Promise執行
的時候,去選中頁面上的文字,發現選中不了!就像卡住了一樣!
從這個方面也說明,js主執行緒
不適合執行復雜的運算,阻塞...
Web Worker進行計算
這裡使用this.$worker.create
方法,搭配this.worker2.postAll
方法
程式碼寫法如下
created() {
// 1. 定義執行緒所用的訊息函式陣列
const actions = [
{
message: "fn1", // message訊息與func函式執行為對映關係
func: (params1, params2) => {
console.log("params引數-->", params1, params2);
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
return fib(30);
},
},
{
message: "fn2",
func: () => {
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
return fib(30);
},
},
{
message: "fn3",
func: () => {
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
// throw "一個報錯掛了,其他的也跟著掛了,走.catch"; // 丟擲錯誤(的確很像Promise.all())
return fib(30);
},
},
];
// 2. 根據訊息函式陣列去create建立一個worker,並存到data變數中去,以便於後續隨時使用
this.worker2 = this.$worker.create(actions);
},
// 點選觸發noParamsFn方法執行
// 使用多個執行緒
noParamsFn() {
this.loadingOne = true;
console.time("多個執行緒計算用時1");
this.worker2
.postAll()
.then((res) => {
console.timeEnd("多個執行緒計算用時1");
console.log("res", res); // 結果是一個陣列 [267914296, 433494437, 701408733]
this.loadingOne = false;
})
.catch((err) => {
console.timeEnd("多個執行緒計算用時1");
console.log("err", err);
this.loadingOne = false;
});
},
我們看一下效果圖
看到沒有。只用了53毫秒
!
- 使用主執行緒去計算
3個fib(30)
的值,需要將近9秒
的時間 - 而使用
Web Worker
去建立三個輔助執行緒分別去運算fib(30)
所需要的時間,只需要50多毫秒
效能提升了100多倍!
這個案例才真正的體現了,Web Worker
開啟多執行緒運算提高效率的強大!
大家可以去筆者的個人網站上去看完整的效果圖:http://ashuai.work:8888/#/myWork
Web Worker
是HTML5
提出的規範
,主流瀏覽器都已經得到了相容。IE
就忽略吧
附上案例完整程式碼
篇幅有限,一些vue-worker
外掛常用的使用語法細節,寫在程式碼中了,大家複製貼上即可使用
<template>
<div class="threadWrap">
<h1>開啟一個執行緒運算用$worker.run方法</h1>
<br />
<el-button
@click="openOneThread"
type="success"
plain
size="small"
style="margin-bottom: 16px"
>計算斐波那契數列值和用時,以及渲染頁面兩個任務</el-button
>
<div>
斐波那契值為:<span class="bold">{{ fibRes }}</span>
<i v-show="btnLoading" class="el-icon-loading"></i>
執行用時:
<i v-show="btnLoading" class="el-icon-loading"></i>
<span class="bold">{{ fibTime }}</span>
毫秒
</div>
<br />
<div class="UI">
<span>不阻塞後續的程式碼執行:</span>
<h2 ref="renderH2"></h2>
</div>
<br />
<br />
<h1>開啟多個執行緒使用$worker.create、postAll方法</h1>
<br />
<el-button
@click="noParamsFn"
type="success"
plain
size="small"
style="margin-bottom: 12px"
:loading="loadingOne"
>不傳參都執行一次</el-button
>
<el-button
@click="byMessageNameStrFn"
type="success"
plain
size="small"
style="margin-bottom: 12px"
:loading="loadingTwo"
>根據message的名字指定誰執行(字串形式)</el-button
>
<el-button
@click="byMessageNameObjParamsFn"
type="success"
plain
size="small"
style="margin-bottom: 12px"
:loading="loadingThree"
>根據message的名字指定誰執行(物件形式可傳參)</el-button
>
<div class="info">F12請開啟控制檯檢視--></div>
<br />
<h1>不使用多執行緒,使用Promise.all太耗時啦!</h1>
<br />
<el-button
@click="usePromiseFn"
type="success"
plain
size="small"
style="margin-bottom: 12px"
>Promise執行多個任務</el-button
>
<div class="info">F12請開啟控制檯檢視--></div>
</div>
</template>
<script>
export default {
name: "myWorkName",
data() {
return {
worker: null,
btnLoading: false,
fibNum: 44,
fibRes: null,
fibTime: null,
/****/
loadingOne: false,
loadingTwo: false,
loadingThree: false,
worker2: null,
};
},
methods: {
/**
* 需求:點選資料計算進行兩個操作
* 第一個UI操作,計算斐波那契數fib(44)的值,且統計計算所用的時長,並寫入到頁面上
* 第二個UI操作,每隔0.1秒,更新頁面上h2標籤的內容值
* 要求兩個操作不阻塞,不能出現後一個UI操作要等待前一個UI操作的情況
* 因為斐波那契是遞迴執行,是一個比較耗時的操作fib(44)約需要近8秒的計算時間
* */
openOneThread() {
/* 第一個UI操作 */
this.btnLoading = true;
this.worker = this.$worker // this.$worker.run(func, [args]?) 一次性的,自動銷燬
.run(
(n) => {
// 注意這裡的函式是內部另一個執行緒空間的,不能從外部引入過來(記憶體空間隔離)
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
let start = Date.now(); // console.time()和console.timeEnd()直接拿不到值,就換種方式
let res = fib(n);
let end = Date.now(); // window.performance.now()也不能用,因為沒window物件
return [res, end - start]; // 返回陣列,第一項是fib(44)的值,第二項是fib(44)遞迴計算用時
},
[this.fibNum] // 引數,從這裡傳遞進去,陣列形式
)
.then((res) => {
console.log("then", res); // 另一個執行緒執行完以後就能拿到結果了
this.fibRes = res[0];
this.fibTime = res[1];
this.btnLoading = false;
})
.catch((err) => {
console.log("err", err);
this.btnLoading = false;
});
/* 第二個UI操作 */
let h2Dom = this.$refs.renderH2;
let n = 0;
let timer = setInterval(() => {
if (n >= 60) {
clearInterval(timer);
} else {
n++;
h2Dom.innerHTML = n;
}
}, 100);
// 使用web worker外掛vue-work的確可以做到不阻塞
},
/**
* 使用多個執行緒
* */
// 呼叫方式一 不傳參
noParamsFn() {
this.loadingOne = true;
console.time("多個執行緒計算用時1");
this.worker2
.postAll()
.then((res) => {
console.timeEnd("多個執行緒計算用時1");
console.log("res", res); // 結果是一個陣列 [267914296, 433494437, 701408733]
this.loadingOne = false;
})
.catch((err) => {
console.timeEnd("多個執行緒計算用時1");
console.log("err", err);
this.loadingOne = false;
});
},
// 呼叫方式二 根據message的名字去指定誰(可多個)去執行 字串形式
byMessageNameStrFn() {
this.loadingTwo = true;
console.time("多個執行緒計算用時2");
this.worker2
.postAll(["fn1", "fn3"]) // 這裡指定"fn1", "fn3"去執行
.then((res) => {
console.timeEnd("多個執行緒計算用時2");
console.log("res", res); // 結果是一個陣列 [267914296, 701408733]
this.loadingTwo = false;
})
.catch((err) => {
console.timeEnd("多個執行緒計算用時2");
console.log("err", err);
this.loadingTwo = false;
});
},
// 呼叫方式三 根據message的名字去指定誰(可多個)去執行 物件形式
byMessageNameObjParamsFn() {
this.loadingThree = true;
console.time("多個執行緒計算用時3");
this.worker2
.postAll([{ message: "fn1", args: ["程式碼修仙路漫漫", "加油幹"] }]) // 這裡指定"fn1" 去執行
.then((res) => {
console.timeEnd("多個執行緒計算用時3");
console.log("res", res); // 結果是一個陣列 []
this.loadingThree = false;
})
.catch((err) => {
console.timeEnd("多個執行緒計算用時3");
console.log("err", err);
this.loadingThree = false;
});
},
/**
* 使用Promise
* */
async usePromiseFn() {
function asyncOne() {
let async1 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async1;
}
function asyncTwo() {
let async2 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async2;
}
function asyncThree() {
let async3 = new Promise(async (resolve, reject) => {
function fib(n) {
if ((n === 1) | (n === 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 1);
}
}
resolve(fib(30));
});
return async3;
}
console.time("使用Promise搭配aysnc和await");
// let paramsArr = [asyncOne()]; // 計算一個大概在3秒左右(計算一次重新整理一次頁面,精確一些)
let paramsArr = [asyncOne(), asyncTwo(), asyncThree()]; // 計算三個耗時任務大概在9秒左右
let res = await Promise.all(paramsArr);
console.timeEnd("使用Promise搭配aysnc和await");
console.log("結果", res);
},
},
created() {
// 1. 定義執行緒所用的訊息函式陣列
const actions = [
{
message: "fn1", // message訊息與func函式執行為對映關係
func: (params1, params2) => {
console.log("params引數-->", params1, params2);
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
return fib(30);
},
},
{
message: "fn2",
func: () => {
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
return fib(30);
},
},
{
message: "fn3",
func: () => {
function fib(n) {
if ((n == 1) | (n == 2)) {
return 1;
} else {
return fib(n - 1) + fib(n - 2);
}
}
// throw "一個報錯掛了,其他的也跟著掛了,走.catch"; // 丟擲錯誤(的確很像Promise.all())
return fib(30);
},
},
];
// 2. 根據訊息函式陣列去create建立一個worker,並存到data變數中去,以便於後續隨時使用
this.worker2 = this.$worker.create(actions);
},
// 別忘了在元件銷燬前清除掉哦
beforeDestroy() {
this.worker = null;
},
};
</script>
<style lang='less' scoped>
.bold {
font-weight: 700;
font-size: 24px;
}
.info {
color: #999;
font-size: 13px;
}
.UI {
display: flex;
align-items: center;
}
</style>
或者大家去筆者的GitHub倉庫
中拉取程式碼,跑起來看,更加方便理解。
GitHub倉庫
地址:https://github.com/shuirongsh...
js中統計程式碼執行時長的三種方式
附帶js
中常用的統計程式執行時長
的三種方式
- window.performance.now()
- https://developer.mozilla.org...
// 計算時間方式一
function fib(n) {
if (n === 1 | n === 2) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
let start = window.performance.now() // 單位毫秒
fib(40)
let end = window.performance.now() // 單位毫秒
console.log((end - start).toFixed(0) + '毫秒');
- Date.now()
- https://developer.mozilla.org...
// 計算時間方式二
function fib(n) {
if (n === 1 | n === 2) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
let start = Date.now() // 單位毫秒
fib(40)
let end = Date.now() // 單位毫秒
console.log((end - start).toFixed(0) + '毫秒');
- console.time() & console.timeEnd()
- https://developer.mozilla.org...
console.time('tom')
console.timeEnd('tom')
A good memory is not as good as a bad pen, record it...