引言
为什么JavaScript是单线程?
什么是Web Worker?
小试牛刀
在单页面应用中使用
注意事项
小结
引言平时小伙伴们不是说日常的项目开发中,都是单纯的搬砖,没啥亮点嘛,那现在就来啦!
咱们今天就来聊聊web worker,这可是面试官最最最喜欢的的性能优化哦~
为什么JavaScript是单线程?总所周知,JavaScript语言的特点是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
什么是Web Worker?为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
在worker线程中,虽然无法直接操作dom节点,也不能使用window对象的默认方法和属性,但是仍然可以使用window对象下的东西,比如websocket,indexedDB等。
workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用postMessage() 方法发送各自的消息,使用 onmessage 事件处理函数来响应消息(消息被包含在Message事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。
关于web worker的兼容性问题,在can i use中查找一轮后发现,基本目前所有主流的浏览器都支持了,因此放心食用,无需考虑兼容性的问题。
小试牛刀前面学习了那么多武功秘籍,少侠们,确定不来一展身手吗?
小羽这里简单的写了一个小demo,这个demo的内容就是递归获取斐波那契数列。会分为单线程和多线程模式,然后分别测试运行20次fb方法所需要的时间。
单线程模式:利用for循环直接执行20次fb,统计执行时间
多线程模式:利用for循环,创建多个worker线程。并使用promise.all处理这些异步的worker线程,等待所有的worker执行完成后,统计执行时间
<!--
* @Author: xiaoyu
* @Description:
* @Date: 2022-05-08 08:40:54
* @LastEditors: xiaoyu
* @LastEditTime: 2022-06-29 23:19:40
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>web worker</title>
</head>
<script>
const number = 20 // 运行次数
// 多线程测试
function workerTest() {
console.log('%c 开始多线程测试 ', 'color:#fff; background:#00897b ')
const workerList = []
for (let i = 0; i < number; i++) {
const workerItem = new Promise((resolve, reject) => {
const myWorker = new Worker('worker.js')
myWorker.postMessage({
function: 'fb',
data: 43
})
myWorker.onmessage = (e) => {
resolve(e.data)
// 关闭worker线程
myWorker.terminate()
}
})
workerList.push(workerItem)
}
console.time('worker多线程执行时间')
Promise.all(workerList).then(res => {
console.log(res)
console.timeEnd('worker多线程执行时间')
})
}
function singleTest() {
console.log('%c 开始单线程测试 ', 'color:#fff; background:#00897b ')
function fb(n) {
if (n === 1 || n === 2) {
return 1;
}
return fb(n - 1) + fb(n - 2)
}
console.time('单线程执行时间')
for (let i = 0; i < number; i++) {
const res = fb(43)
console.log({
data: res,
name: 'single test'
})
}
console.timeEnd('单线程执行时间')
}
</script>
<body>
<button onclick="singleTest()">单线程测试</button>
<button onclick="workerTest()">多线程测试</button>
</body>
</html>
/*
* @Author: xiaoyu
* @Description: worker 线程
* @Date: 2022-05-08 08:41:30
* @LastEditors: xiaoyu
* @LastEditTime: 2022-06-29 23:17:44
*/
// 方法对象
const funcObj = {
fb: (n) => {
if(n===1 || n ===2){
return 1;
}
return funcObj.fb(n-1) + funcObj.fb(n-2)
}
}
// onmessage事件
onmessage = function(e){
const {data} = e;
const res = funcObj[data.function](data.data)
// 将获取的数据通过postMessage发送到主线程
self.postMessage({
data: res,
name: 'worker test'
})
self.close()
}
打开任务管理器,点击单线程测试按钮进行单线程的测试。可以从下图发现,单线程的调用时间约为70s,cpu的调用基本上也就只是两个核心在切换工作,小羽在多次测试后,其实是有多个核心在切换工作,不过单一时间只有一个核心是在满载工作(递归获取斐波那契数列)。
同样是打开任务管理器,然后点击多线程测试按钮。此时咱们的cpu就不再偷懒了,直接16线程满载运行,只需要7.9s就完成了20次递归获取斐波那契数列。
咱们简单的计算一下使用web worker多线程提升效果:(70750-7973)/7973 ≈7.87。即提升了7.87倍的效率。当然这是在8核16线程上的电脑上跑了,如果在核心数不同的cpu上这个倍数也是会发生相应的变化
在单页面应用中使用通过上面的例子,是不是so easy呀?
好啦,那咱们就算掌握了web worker的基本使用方法啦。
但是在react、vue等单页面应用中,webpack/vite通常会将js代码打包成一个js文件。因此通过上面的new Worker('worker.js')的方式来新建worker,将会报访问不到worker.js的错误。
所以,在单页面应用中,咱们该怎么使用web worker呢?
方案1:既然webpack/vite会将js的代码打包成一个js文件,那咱们不让它打包不就好了。而单页面应用的工程下,通常都是会有一个public的静态资源目录,咱们将worker.js放入其中即可。
方案2:webpack4及以下的版本可以使用worker-loader
方案3:webpack5/vite则可以使用new Worker(new URL('worker.js', import.meta.url))的方式
import React from 'react'
export default function WebWorkerTest() {
const handleClick = () => {
const number = 1
const workerList = []
console.log('%c 开始多线程测试 ', 'color:#fff; background:#00897b ')
for (let i = 0; i < number; i++) {
const workerItem = new Promise((resolve, reject) => {
const myWorker = new Worker(new URL('../utils/fb.worker.ts', import.meta.url))
myWorker.postMessage({
function: 'fb',
data: 43
})
myWorker.onmessage = (e) => {
resolve(e.data)
// 关闭worker线程
myWorker.terminate()
}
})
workerList.push(workerItem)
}
console.time('worker多线程执行时间')
Promise.all(workerList).then(res => {
console.log(res)
console.timeEnd('worker多线程执行时间')
})
}
return (
<>
<button onClick={handleClick}>vite/webpack5</button>
</>
)
}
// fb.worker.ts
// 方法对象
const funcObj = {
fb: (n: number): number => {
if (n === 1 || n === 2) {
return 1;
}
return funcObj.fb(n - 1) + funcObj.fb(n - 2);
},
};
// onmessage事件
onmessage = function (e) {
const { data } = e;
const res = funcObj[data.function](data.data);
// 将获取的数据通过postMessage发送到主线程
self.postMessage({
data: res,
name: "worker test",
});
self.close();
};
注意事项
虽然web worker可以调用cpu的多线程,从而提高咱们页面的性能。但是它不是随便使用的,如果滥用web worker可能不仅不会得到性能的提升,还可能造成性能的损耗。
举一个简单的小栗子
如果咱们将递归获取斐波那契数列第n位的方法,将传入参数修改为第2位,这时候咱们再重跑单线程测试和多线程测试。
结果如下图,咱们可以发现单线程模式下,获取20次斐波那契数列第二位的时间仅需要1.5ms,而在多线程的情况下却需要78ms。这是为什么呢?因为咱们每次创建worker线程以及possmessage通信都是需要损耗一些性能以及时间的。因此web worker是不可以滥用的哦,日常开发中,建议在需要消耗比较多的cpu运算能力的时候酌情使用。
小结本文通过了几个简单的小栗子,带大家学习web worker的基本知识,使用方法,以及需要注意的事项。小伙伴们在日常的开发中可以按需尝试哦,让自己的项目中多些亮点,也可以让面试官眼前一亮哦。
更多关于web worker使用的资料请关注易知道(ezd.cc)其它相关文章!