引言
平时小伙伴们不是说日常的项目开发中,都是单纯的搬砖,没啥亮点嘛,那现在就来啦!
咱们今天就来聊聊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使用的资料请关注阿兔在线工具其它相关文章!