Web Worker 是什么
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。so bad,单线程现在基本是浪费CPU性能的
HTML5 提供并规范了 Web Worker 这样一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程(Worker 线程)中
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行
这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程就会很流畅,不会被阻塞或拖慢。
Web Worker 的分类
Web Worker 根据工作环境的不同,可分为专用线程 Dedicated Worker和共享线程 Shared Worker。
Dedicated Worker的Worker只能从创建该Woker的脚本中访问
SharedWorker则可以被多个脚本所访问
在开发中如果使用到 Web Worker,目前大部分主要还是使用 Dedicated Worker的场景多,它只能为一个页面所使用,本文讲的也是这一类;而Shared Worker可以被多个页面共享,为跨浏览器 tab 共享数据提供了一种解决方案。
Web Worker的使用限制
同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
文件限制
Worker 线程无法读取本地文件(file://),会拒绝使用 file 协议来创建 Worker实例,它所加载的脚本,必须来自网络。
DOM操作限制
Worker 线程所在的全局对象,与主线程不一样,区别是:
- 无法读取主线程所在网页的 DOM 对象
- 无法使用
document、window、parent这些对象
通信限制
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成,交互方法是postMessage和onMessage,并且在数据传递的时候, Worker 是使用拷贝的方式。
脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求,也可以使用setTimeout/setInterval等API
基本 API
主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。
const worker = new Worker(aURL, options);worker.postMessage: 向 worker 的内部作用域发送一个消息,消息可由任何 JavaScript 对象组成worker.terminate: 立即终止 worker。该方法并不会等待 worker 去完成它剩余的操作;worker 将会被立刻停止使用完毕,为了节省系统资源,必须关闭 Worker。
// 主线程 worker.terminate(); // Worker 线程 self.close();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) // 发生错误时所在脚本文件的行号 })
常见的使用方式
1. 直接指定脚本文件
const myWorker = new Worker(aURL, option)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:
Worker 线程内部需要有一个监听函数,监听message事件。
// 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 world2. 使用 Blob URL 创建
除了这种通过引入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 的功能并将其扩展以支持用户系统上的文件。
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)载入与主线程在同一个网页的JavaScript 脚本代码
嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,比如下面的app/worker
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>然后,读取这一段嵌入页面的脚本,用 Worker 来处理。
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
};Worker 线程中引入脚本
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');战应用场景
处理大量CPU耗时计算操作
大家最关心的还是 Web Worker 实战场景,开头我们说到,当有大量复杂计算场景时,可使用 Web Worker
<!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>worker计算</title>
</head>
<body>
<div>计算从 1 到给定数值的总和</div>
<input type="text" placeholder="请输入数字" id="num" />
<button onclick="calc()">开始计算</button>
<span>计算结果为:<span id="result">-</span></span>
<div>在计算期间你可以填XX表单</div>
<input type="text" placeholder="请输入姓名" />
<input type="text" placeholder="请输入年龄" />
<script>
function calc() {
const num = parseInt(document.getElementById('num').value)
let result = 0
let startTime = performance.now()
// 计算求和(模拟复杂计算)
for (let i = 0; i <= num; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
const time = performance.now() - startTime
console.log('总计算花费时间:', time)
document.getElementById('result').innerHTML = result
}
</script>
</body>
</html>如上,第一个输入框与按钮是负责模拟复杂计算的,比如输入 10000000000,点击开始计算,这时主线程处理一直在处理同步计算逻辑,在完成计算之前,会发现页面处于卡顿的状态,下方的两个输入框也无法点击交互,在我的电脑这部分计算是花了14s左右,这个卡顿时间给用户的体验就很差了。
打开控制台调用也可以看到这里CPU使用率是100%
如果把这部分计算交给 Web Worker 来处理,修改代码:
<script>
const worker = new Worker('./worker.js')
function calc() {
const num = parseInt(document.getElementById('num').value)
worker.postMessage(num)
}
worker.onmessage = function (e) {
document.getElementById('result').innerHTML = e.data
}
</script>./worker.js
function calc(num) {
let result = 0
let startTime = performance.now()
// 计算求和(模拟复杂计算)
for (let i = 0; i <= num; i++) {
result += i
}
// 由于是同步计算,在没计算完成之前下面的代码都无法执行
const time = performance.now() - startTime
console.log('总计算花费时间:', time)
self.postMessage(result)
}
self.onmessage = function (e) {
calc(e.data)
}然后重复上述一样的操作,输入 10000000000 计算,会发现下方两个输入框可正常流畅输入,整个页面也不卡顿。
Worker 运行独立于主线程的后台线程中,分担执行了大量占用CPU密集型的操作(但运行时间并不会变短),解放了主线程,主线程就能及时响应用户操作而不会造成卡顿的现象。使用Web Worker后,控制台工具可看到CPU使用率处于较低正常水平,计算过程跟没计算之前的水平一样。