阅读 2611

在业务中做出的Worker实践[堪称勇士] - 七日打卡

大家看到标题勇士二字不知是否有所疑惑。我这里不做太多解释,只说一句:因为这个是之前的老项目(大家懂吗old very old),那真的可以说是牵一发而动全身啊

但是呢,很快啊,我啪的一下就决定了,直接就选中一个文件CTRL+A,然后Delete了啊,这波操作可以说是意气风发,无比自信。 虽然我后来为自己的自信付出了亿点点代价,但是还好结果令我欣慰,今天也是对这次勇士行为的Worker部分做一个总结记录。

今天从三个方向来描述Worker:

  • what(worker是什么?)
  • why(为什么要使用?)
  • how(怎么用?)

What - worker是什么?

我们都知道JavaScript是一门单线程的语言,也就是说所有的任务只能在同一个线程上执行,同一个时间内只能执行一个任务,这就造成了一个问题,当线程中任务过多时,就会导致任务的阻塞。而随着我们现在无论是硬件的发展(多核CPU),还是前端系统(需求复杂,功能众多)的发展,这都会造成众多不便,而Worker就是为了解决这些问题而来的。

概述

Worker可以创建一条独立于主线程之外的另一线程。通过Worker创建的这个线程运行在后台,主线程可以将一些任务分配给这一独立线程去执行,两者互不干扰,当在Worker线程中的任务执行完毕后可以通过message来向主线程发送执行结果。 因为Worker是独立运行的线程,所以它在执行时全局上下文是与我们平常使用的全局window对象不同的,根据Worker全局上下文的不同又可以分为两种:

  1. Worker:专用Worker,仅供当前执行脚本的页面使用,当这个页面关闭时,当前worker也会随之关闭,对应的上下文为DedicatedWorkerGlobalScope
  2. sharedWorker:共享Worker,可以供多个页面共同使用,当所有关联页面关闭时,当前worker才会被关闭。对应的上下文为SharedWorkerGlobalScope

我当时使用的是专用Worker,我们在平时使用的时候应该也是使用该Worker更多一些,所以本文只讨论这个专用的Worker。

注意点

首先我们来看看怎么来创建一个简单的worker对象吧

const worker = new Worker( url )
复制代码

1. 同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

2. API访问限制

我们上面已经提到了 Worker 线程所在的全局对象,与主线程不一样,所以无法读取主线程上的一些对象和函数。我们这里来简单列出几种常见的

  • 不可用:
    • window
    • document
    • alert()
    • confirm()
  • 可用:
    • navigator
    • location
    • XMLHttpRequest
    • fetch

更详细的API访问权限可以移步官网查看:点这里直达

3. 文件限制

worker无法访问本地的资源,所传入的URL必须要是网络资源。所以对于本地的脚本我们可以将其转换成Blob URL使用

Why - 为什么要使用?

其实在上文中,我已经简单有提到Worker是可以分担主线程上一些计算量大,耗费时间长的任务的,这也是我们需要使用它的主要原因。

针对我接受的这个项目来说,我使用Worker的出发点主要是因为以下两点:

  • 项目中有一个可视化大屏,大屏中涵盖了很多数据,包括组织数据,地图数据,以及各种统计数据等。这些数据量极大,尤其是地图以及组织结构的数据,其种不乏一些对地图数据和组织数据的遍历操作。最最最重要的是这些接口的请求都是在登录之后去请求的,就导致一个很严重的问题,大屏需要在7-10s左右才会完全展现出来,虽然中间有个loading的过渡效果,但是在我看来仍然体验很差。

  • 项目中有一个轮询接口,需要定时去查询数据来更新页面。这就导致在频繁的请求下,一些点击的交互会出现一丝丝的卡顿。

How - 怎么用?

因为公司项目代码没办法拿出来给大家看。所以我们在这里做一个简单的示例带大家感受一下

实现步骤:

1. 创建一个index.html文件

  <script type="module">
    // 引入worker脚本
    import workerScript from './worker.js'
    // 创建worker线程,参数为脚本的url
    // 这里需要注意一下,这里的url是Blob URL,具体原因我们在上文已经说过
    const worker = new Worker(workerScript);
    // 主线程和子线程进行通信
    worker.postMessage('http://juejin.cn')
    // 获取子线程发送来的消息
    worker.onmessage = (msg) => {
      const { data } = msg
      document.body.innerHTML = data
    }
    // 监听worker的错误事件
    worker.onmessageerror = (error) => console.log(error)
  </script>
复制代码

2. 创建一个worker.js文件

  // 需要在worker中执行的脚本
  const workerScript = () => {
    onmessage = (msg) => {
      if (msg.data) {
      	// 接收到主线程的消息之后,与主线程进行通信
        postMessage(`我接受到你的参数了,我再吐给你${msg.data}`);
      }
    };
  };

  // 将上述执行脚本转换为字符串
  let code = workerScript.toString();
  // 对字符串进行分割,取到onmessage部分的函数声明
  code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"));
  // 将字符串转换为Blob URL
  const blob = new Blob([code], { type: "application/javascript" });
  const blobUrl = URL.createObjectURL(blob);
  // 导出生成的Blob URL
  export default blobUrl;
复制代码

这个文件里需要再次强调一下,我们在创建Worker的时候只能去执行网络资源,不能加载本地资源,所以当我们单纯的导出这个js文件的时候会报类似资源加载失败的错误。

我们下面所执行的操作就是将执行代码转换成Worker可识别的Blob URL

执行效果

利用worker进行网络请求

稍微对worker.js做出一些调整,即可实现

  // 需要在worker中执行的脚本
  const workerScript = () => {
    onmessage = (msg) => {
       // 发送网络请求
      const response = await fetch(msg.data, {
        method: "post",
        body: `yes = 1`,
      }).then((res) => res.json);

      if (response) {
      	// TODO 可以在这里对数据进行处理,然后发送给主线程
        postMessage(`我数据处理完成了,给你用吧`);
      }
  };

  // 将上述执行脚本转换为字符串
  let code = workerScript.toString();
  // 对字符串进行分割,取到onmessage部分的函数声明
  code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"));
  // 将字符串转换为Blob URL
  const blob = new Blob([code], { type: "application/javascript" });
  const blobUrl = URL.createObjectURL(blob);
  // 导出生成的Blob URL
  export default blobUrl;
复制代码

我们可以看到这个请求和我们平时在该面板中看到的请求有一些不同,它的前面带了一个小齿轮,这就证明这个请求是在worker线程中进行请求的,到这里我们就大功告成了。

在项目中的实践

通过以上的操作,在这个项目中,登录之后可视化大屏的加载速度稳定在 3~5s,速度提升了40% 左右。 当然这只是优化中的一个小点,当然我在项目中也使用了React的memouseMemouseCallback等方法进行了相应的优化。

上面的worker.js是我在项目中做的一个封装,所以你也可以直接用到你的项目中去。 使用步骤:

  1. worker.js文件复制进当前项目的相关文件夹中
  2. 在TODO注释的下面可以添加上自己对项目中大数据的复制处理,比如遍历、递归等,处理完成后通过postMessage传递给主线程
  3. 在主文件中通过import xxx from './worker'引入执行脚本
    • 通过new Worker(xxx)创建Worker线程
    • 通过onmessage方法监听线程间的通信事件(一般是将Worker线程传递过的处理好的数据进行展示)

总结

我们可以看到,其实Worker的使用方法很简单,只需要在合适的项目中多添加那么几行代码就可以完成一些影响页面性能的操作,何乐而不为呢?

而且对于代码层面的性能优化这个点来说,我觉得作为程序员我们在日常撸代码的时候是要去思考的,不能单纯的为了实现需求而去啪啪敲,有时候也要想一下如果这段代码进行的操作已经影响到了页面的交互,那么是否有办法在代码层面进行优化呢?(这好像是一个老生长谈的问题,但是确实在理!俗话说的好“不听老人言吃亏在眼前”)。如果有优化的可能那么就去做相关技术的调研、方案的制定,这些对个人成长感觉十分有利(都是从大佬那里学来的QAQ)

日常的思考固然重要,但是如果将思考做一定的产出的话,那效果将十分nice~double up!(自己有在做,所以有亲身感受)

最后说句:xdm,加油淦!

文章分类
前端
文章标签