- 问题描述
- 问题分析
- 基本用法
- 解决思路
- 具体实现
- 应用效果
- 总结
问题起源于我们的业务,在我们页面中当点击按钮后我们会打开一个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 对象,也无法使用document
、window
、parent
这些对象。但是,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
})
})
}
开源的效果:
我们可以看到,通过将密集逻辑计算迁移到web workers之后,我们的UI主线程没有被阻塞,可以正常的做loading加载,信息输入等操作。
本文主要总结了一下Web Worker在使用过程中,个人认为不错的一些实践,虽然并不涉及更复杂以及完善的用法(比如Worker和Worker之间的通信),但是目前我个人用Web Worker用得还比较少,本文算是对之前的实际业务场景出现的问题进行了修复,如果我们发现在现有的条件下无法改善问题。那么可以尝试跳出当前的圈内去寻找更好用的方式。
[[译] JavaScript 工作原理:Web Worker 的内部构造以及 5 种你应当使用它的场景](https://juejin.cn/post/6844903566281457678)