有一些前端技术点,即使以前用过,但没有自己动手归纳总结过,许久还是要回过头来还是需要重新梳理。于是,本文就来梳理一下 Web Worker。
由于JavaScript语言采用的是单线程,同一时刻只能做一件事,如果有多个同步计算任务执行,则在这段同步计算逻辑执行完之前,它下方的代码不会执行,从而造成了阻塞,用户的交互也可能无响应。
但如果把这段同步计算逻辑放到Web Worker执行,在这段逻辑计算运行期间依然可以执行它下方的代码,用户的操作也可以响应了。
HTML5 提供并规范了 Web Worker 这样一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程(Worker 线程)中。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程就会很流畅,不会被阻塞或拖慢。
Web Worker 根据工作环境的不同,可分为专用线程 Dedicated Worker和共享线程 Shared Worker。
Dedicated Worker的Worker只能从创建该Woker的脚本中访问,而SharedWorker则可以被多个脚本所访问。
在开发中如果使用到 Web Worker,目前大部分主要还是使用 Dedicated Worker的场景多,它只能为一个页面所使用,本文讲的也是这一类;而Shared Worker可以被多个页面共享,为跨浏览器 tab 共享数据提供了一种解决方案。
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
Worker 线程无法读取本地文件(file://
),会拒绝使用 file 协议来创建 Worker实例,它所加载的脚本,必须来自网络。
Worker 线程所在的全局对象,与主线程不一样,区别是:
document
、window
、parent
这些对象Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成,交互方法是postMessage
和onMessage
,并且在数据传递的时候, Worker 是使用拷贝的方式。
Worker 线程不能执行alert()
方法和confirm()
方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求,也可以使用setTimeout/setInterval
等API
const worker = new Worker(aURL, options);
worker.postMessage
: 向 worker 的内部作用域发送一个消息,消息可由任何 JavaScript 对象组成worker.terminate
: 立即终止 worker。该方法并不会等待 worker 去完成它剩余的操作;worker 将会被立刻停止worker.onmessage
:当 worker 的父级接收到来自其 worker 的消息时,会在 Worker 对象上触发 message 事件worker.onerror
: 当 worker 出现运行中错误时,它的 onerror 事件处理函数会被调用。它会收到一个扩展了 ErrorEvent 接口的名为 error 的事件worker.addEventListener('error', function (e) {console.log(e.message) // 可读性良好的错误消息console.log(e.filename) // 发生错误的脚本文件名console.log(e.lineno) // 发生错误时所在脚本文件的行号
})
const myWorker = new Worker(aURL, options);
aURL
表示 worker 将执行的脚本的 URL(脚本文件), 即 Web Worker 所要执行的任务。
案例如下:
// 主线程下创建worker线程
const worker = new Worker('./worker.js')
// 监听接收worker线程发的消息
worker.onmessage = function (e) {console.log('主线程收到worker线程消息:', e.data)
}
// 向worker线程发送消息
worker.postMessage('主线程发送hello world')
worker.js
:
// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {// e.data表示主线程发送过来的数据self.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});
Web Worker 的执行上下文名称是 self,无法调用主线程的 window 对象的。上述写法等同于以下写法:
this.addEventListener("message", function (e) {// e.data表示主线程发送过来的数据this.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});
将JS文件引入html挂在本地开发环境运行,运行结果如下:
主线程收到worker线程消息: worker线程收到的:主线程发送hello world
除了这种通过引入js文件的方式,也可以通过URL.createObjectURL()
创建URL对象,创建内嵌的worker
/**
* const blob = new Blob(array, options);
* Blob() 构造函数返回一个新的 Blob 对象。blob 的内容由参数数组中给出的值的串联组成。
* @params array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array
* @options type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。还有两个这里忽略不列举了
*/
/**
* URL.createObjectURL():静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象
*/
const worker = new Worker(URL.createObjectURL(blob));
- Blob 对象表示一个不可变、原始数据的类文件对象,它的数据可以按文本或二进制的格式进行读取。File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。
- Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL 方法来创建 Blob URL,该方法接收一个 Blob 对象,并为其创建一个唯一的 URL,其形式为
blob:
/ - 浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL 到 Blob 映射。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效,它保存在内存中的。它允许引用
、
中的 Blob,但如果你访问的 Blob URL 不再存在,则会从浏览器中收到 404 错误
function func() {console.log('hello')
}
function createWorker(fn) {// const blob = new Blob([fn.toString() + ' fn()'], { type: 'text/javascript' })const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })return URL.createObjectURL(blob)
}
createWorker(func)
Worker线程内部要加载其他脚本,可以使用 importScripts()
// worker.js
importScripts("constants.js");
// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {self.postMessage(foo); // 可拿到 `foo`、`getAge()`、`getName`的结果值
});
// constants.js
const foo = "变量";
function getAge() {return 25;
}
const getName = () => {return "jacky";
};
还可以同时加载多个脚本
importScripts('script1.js', 'script2.js');
大家最关心的还是 Web Worker 实战场景,开头我们说到,当有大量复杂计算场景时,可使用 Web Worker
worker计算 计算从 1 到给定数值的总和计算结果为:-在计算期间你可以填XX表单
如上,第一个输入框与按钮是负责模拟复杂计算的,比如输入 10000000000,点击开始计算,这时主线程处理一直在处理同步计算逻辑,在完成计算之前,会发现页面处于卡顿的状态,下方的两个输入框也无法点击交互,在我的电脑这部分计算是花了14s左右,这个卡顿时间给用户的体验就很差了。
打开控制台调用也可以看到这里CPU使用率是100%
如果把这部分计算交给 Web Worker 来处理,修改代码:
./worker.js
function calc(num) {let result = 0let startTime = performance.now()// 计算求和(模拟复杂计算)for (let i = 0; i <= num; i++) {result += i}// 由于是同步计算,在没计算完成之前下面的代码都无法执行const time = performance.now() - startTimeconsole.log('总计算花费时间:', time)self.postMessage(result)
}
self.onmessage = function (e) {calc(e.data)
}
然后重复上述一样的操作,输入 10000000000 计算,会发现下方两个输入框可正常流畅输入,整个页面也不卡顿。
Worker 运行独立于主线程的后台线程中,分担执行了大量占用CPU密集型的操作(但运行时间并不会变短),解放了主线程,主线程就能及时响应用户操作而不会造成卡顿的现象。使用Web Worker后,控制台工具可看到CPU使用率处于较低正常水平,计算过程跟没计算之前的水平一样。
这个是我工作中遇到的场景,通过 绘制 WebRTC 视频流录制视频,最后生成视频。
以前写过一篇文章如何实现前端录屏,这篇基本就没怎么认真写,就是纯属记录,而且现在的代码跟这篇文章的示例代码差别很大(就是说示例代码改进空间很大),建议总体看下思路,实现具体细节就不细究了,毕竟思路相通。
就以上面这篇文章的代码作为示例(现最新代码跟公司代码业务结合就不放了),有个比较耗CPU的操作
// 16ms一次的定时器
refreshTimer.current = window.setInterval(onRefreshTimer, 16)
// onRefreshTimer 函数里面做的实际就是高频执行 recorderDrawFrame() 方法
// 录屏绘制操作
const recorderDrawFrame = () => {const $recorderCanvas = recorderCanvas.current!const $player = videoRef.current!const ctx = recorderContext.current!const { width, height } = getResolution()$recorderCanvas.width = width$recorderCanvas.height = height// 其中这个绘制函数对CPU占用率会比较高(在低配置的电脑浏览器上)ctx.drawImage($player,0,0,$player.videoWidth,$player.videoHeight,0,0,$recorderCanvas.width,$recorderCanvas.height,)drawWatermark(ctx, width)
}
那么怎么做优化呢?就是把整个 onRefreshTimer
这个定时器函数交给 Web Worker执行。
上面说到,Web Worker 虽有DOM操作的限制,但可以使用 setTimeout/setInterval
等API,所以具体实现就是把 Worker 封装为类,在类内部处理好逻辑,然后暴露 setInterval 等方法给外部实例调用。
if (worker) {refreshTimer.current = worker.setInterval(onRefreshTimer, 16)
}
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享