引言

平时小伙伴们不是说日常的项目开发中,都是单纯的搬砖,没啥亮点嘛,那现在就来啦!

咱们今天就来聊聊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使用的资料请关注阿兔在线工具其它相关文章!

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部