Web Worker 是什么
# 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 world
# 2. 使用 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使用率处于较低正常水平,计算过程跟没计算之前的水平一样。