1. 问题描述
  2. 问题分析
  3. 基本用法
  4. 解决思路
  5. 具体实现
  6. 应用效果
  7. 总结

问题起源于我们的业务,在我们页面中当点击按钮后我们会打开一个loading展示。

这个loading如上所示,可以看到loading中有计时。(内部使用setInterval)来计算秒数。

但实际上,我们发现。在调用了提取按钮的触发了提取逻辑后,发现这个setInterval计时并不走,第一条内容完毕的画面也不会展示。

因为整个提取逻辑中,大量的使用了字符串处理以及将文件内容转为ast,各种字符串判断、修改、删除的处理,计算量十分巨大。

直到提取逻辑运行完毕后,整个loading展示组件会在短时间内计算完毕。

看起来的效果也就是1→2→3以较快的速度走完了,然后整个弹窗关闭了。

在这个过程中,不光伴随着秒数不走的情况,还伴随着鼠标的输入事件生效缓慢。比如按下右键弹出菜单/滑动选中文本,往往需要等3-5秒 甚至是整个提取逻辑结束后才会将菜单展示出来。

还有Input输入框的输入事件卡顿,当你从键盘输入后,也是需要等待1-3秒,你输入的内容才会展现在输入框中。

该如何解决上诉卡顿的问题呢?

我们都知道JS是作为单线程编程语言,一次只能执行一组指令,意味着在执行下一个进程之前,所有的其他进程都必须等待一条指令的完成。

如果我们在主线程中执行了繁重的计算任务,并且还希望用户依然能够跟我们的应用进行交互,那么我们就需要希望大量的计算任务不会阻塞UI进程。

注意:阻塞是指串行处理,一次执行一项操作。另一方面,非阻塞代码(异步)可以并行运行(或多线程)。

要让大量的计算不阻塞我们的UI线程,我们就需要Web Workers 。Web Workers 允许我们主线程需要他的时候调用执行,在后台线程中计算大量繁重的任务,并在任务完成的时候通知主线程。

Web Workers 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。

等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

我们先来看一下Web Worker的基本用法。

|

// worker.js
onmessage = function(e) {
    console.log(`Message received from main script: '${e.data}'.`);
    postMessage('Message received.');
};

|

// index.js
const w = new Worker('worker.js');
w.postMessage('hello worker'); // send a message to worker
w.onmessage = function(e) {
    console.log(`Message received from worker: '${e.data}'`)
};

|

<!-- index.html -->
<script src="./index.js"></script>

worker.js有一个onmessage处理事件和一个用于发消息的postMessage的全局函数,当收到主线程发来的消息时,onmessage会被自动触发执行,我们可以通过e.data获得发送过来的数据。在index.js中,我们使用Worker创建了一个对象,注意Worker构造函数接收的参数是Worker文件的路径,获得Worker对象之后,具体的操作和worker.js类似,只是这里的postMessage和onmessage都是属于Worker对象的方法。

可以看出Web Worker的基本用法并不复杂,当然还有Web Worker之间的消息传递(MessageChannel),Web Worker的关闭(主线程中w.terminate()或者Worker中的close())等使用细节,大家可以根据需要去查阅MDN的相关资料

附上阮一峰的教程。https://www.ruanyifeng.com/blog/2018/07/web-worker.html

那么,针对于实际业务中,我们可以将提取部分的密集计算脚本迁移到我们的worker文件中,通过主线程发送postMessage消息通知worker调用提取逻辑。

但需要注意的是,Web Worker 有以下几个使用注意点。

(1)同源限制

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

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

具体实现我们根据上面说到的几个限制点,来改造我们的提取逻辑。

因为海外制作的提取逻辑中大量包含了dom相关的运算,迁移成本非常高。所以这次实际应用我使用了主题制作(Theme)的提取逻辑进行修改,根据询问得知,主题制作的提取逻辑中只有规则查询部分使用了dom的

querySelector,用于匹配指定的DOM做操作。(而且这部分性能开销并不大,出于测试原因,我将这部分的处理逻辑直接注释掉了。)想要在Vue中使用Web Workers,我们需要使用webpack的worker-loader包装器,用于处理我们的worker文件。

step1. 安装worker-loader

|

npm install worker-loader --save-dev

step2. 使用worker-loader

|

  chainWebpack: (config) => {
    config.module
      .rule('worker')
      .test(/\.worker\.js$/)
      .use('worker-loader')
      .loader('worker-loader')
      .options({
        inline: 'fallback',
      })
    config.module.rule('js').exclude.add(/\.worker\.js$/)
}

step3. 创建worker文件并使用

|

<script>
import Parser from './parser/parser.worker.js'
// created
this.parser = new Parser()
// created
// method 
let data = {
...需要处理的业务数据
}
this.parser.postMessage({ command: 'wholeStart', data }) this.parser.onmessage()
this.parser.onmessage = ({ data }) => {
     if (data.command === 'wholeStart' && data.status === 'success') {
         this.$Message.success('提取成功!')
     }
}
// method</script>

|

const Parser = class {
...省略业务功能代码
}
const parser = new Parser()
self.onmessage = (e) => {
    parser[e.data.command](e.data.data).then(res=>{
        postMessage({
            command: e.data.command,
            status: 'success',
            data: res
        })
    })
}

开源的效果:

http://afshinm.github.io/50k/

我们可以看到,通过将密集逻辑计算迁移到web workers之后,我们的UI主线程没有被阻塞,可以正常的做loading加载,信息输入等操作。

本文主要总结了一下Web Worker在使用过程中,个人认为不错的一些实践,虽然并不涉及更复杂以及完善的用法(比如Worker和Worker之间的通信),但是目前我个人用Web Worker用得还比较少,本文算是对之前的实际业务场景出现的问题进行了修复,如果我们发现在现有的条件下无法改善问题。那么可以尝试跳出当前的圈内去寻找更好用的方式。

Web Worker的一些实践

Web Worker在Vue中的实际应用

使用 Web Workers

一文搞懂 Web Worker(原理到实践)

[[译] JavaScript 工作原理:Web Worker 的内部构造以及 5 种你应当使用它的场景](https://juejin.cn/post/6844903566281457678)

最后修改:2023 年 03 月 24 日
如果觉得我的文章对你有用,请随意赞赏