JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。随着电脑计算能力的增强,尤其是多核CPU的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到Worker线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责UI交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较浪费资源,不应该过渡使用,而且一旦使用完毕,就应该关闭。
值得注意的是,Worker 与 JavaScript 的异步编程有着本质区别。异步编程是利用 Event Loop 机制,将异步回调的任务暂时放在后面的任务队列中,等 JavaScript 引擎执行完前面的所有任务之后,再对其进行执行,其本质还是单线程,如果回调任务需要消耗较多资源,一样会阻塞后面等待的任务;但 Worker 可以开启一个独立于主线程的线程,二者互不干扰;Worker 线程执行完任务之后,直接把计算结果返回给主线程即可。下图是 Web Worker 和主线程之间的通信方式:

Web Worker 的意义在于可以将一些耗时的数据处理操作从主线程中剥离,使主线程更加专注于页面的渲染和交互。基于 Web Worker 的特性,以下场景可以考虑使用 Web Worker:
前面铺垫了这么多,那么Web Worker到底怎么用呢?先来个小demo体验下,跟着我做个简易(简陋)的计数器来看看Worker是怎么创建的,以及Worker线程和主线程之间是怎么通信的
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>计数器</title>
</head>
<body>
<p>计数: <output id="result"></output></p>
<button onclick="startWorker()">开始 Worker</button>
<button onclick="stopWorker()">停止 Worker</button>
<br /><br />
<script>
var w;
var workerTimeout;
function startWorker()
{
// 检测浏览器是否兼容 Web Worker
if(typeof(Worker)!=="undefined") {
// 实例化 Worker
w=new Worker("/static/worker.js");
// 主线程 接受 Worker 传来的信息
w.onmessage = function (event) {
document.getElementById("result").innerHTML=event.data.num;
workerTimeout = event.data.workerTimeout;
};
}
else {
document.getElementById("result").innerHTML="Sorry, your browser does not support Web Workers...";
}
}
function stopWorker()
{
w.terminate();
clearTimeout(workerTimeout);
}
</script>
</body>
</html>// worker.js
var i=0;
function timedCount (){
i=i+1;
var workerTimeout = setTimeout("timedCount()",500);
// Worker 线程向主线程发送消息
postMessage({
num: i,
workerTimeout: workerTimeout
});
}
timedCount();// app.js
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const serve = require('koa-static');
// 管理静态资源
app.use(serve(__dirname))
const fs = require('fs');
app.use(async (ctx, next) => {
ctx.type = 'text/html';
// 渲染主页面
ctx.body = fs.createReadStream('./index.html');
});
app.listen(8000);// package.json
{
"name": "worker-demo-simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "hotnode app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"koa": "^2.13.0",
"koa-static": "^5.0.0"
}
}至此,我们的“计数器”小项目就搭建完成了,赶紧在根目录下执行 npm start 跑起来试试看吧,效果如下:

在本地访问 localhost:8000 即可看到我们用 Web Worker 创建的“计数器”,点击“开始 Worker”,计数值会以大约500ms的时间间隔递增,点击“停止 Worker”即可注销 Worker。
前面的 index.html 和 worker.js 中包含了 Web Worker 最基础的API用法;其中,在主线程使用 new 操作符,调用 Worker() 构造函数,可以新建一个 Worker 线程;Worker() 构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于Worker读取的脚本必须来自网络,demo 中的 js 脚本放在本地的 node 服务器中。
Worker 线程调用 postMessage() 方法,可以向主线程发送消息,消息内容可以是各种数据类型,包括二进制数据。同样,主线程也可以调用 worker.postMessage() 方法,向 Worker 线程发送消息。
主线程可以通过 worker.onmessage() 方法监听 message 事件,以获取 Worker 线程传来的消息;同理 Worker 线程也可以使用 self.onmessage() 方法监听 message 事件来获取主线程传来的消息。
如果想关闭 Worker 线程,可以在主线程调用 worker.terminate(),或者在 Worker 线程调用 self.close() 来关闭 Worker。这两种方法是等效的,但比较推荐的用法是在 Worker 线程里通过 self.close() 关闭 Worker,以防止在主线程意外关闭正在运行的 Worker。
有了这几个基本的 API,就可以实现简单的 Worker 线程与主线程之间的通信了,完整的 Web Worker API 请移步 MDN。
什么?到现在为止还没看到 Web Worker 实际的功效?别急,跟着我再做个demo,咱们一起见证下 Web Worker 强大的多线程功效吧。
之前实习的时候所在团队是做地图 api 的,其中 3D 地图是使用 WebGL 技术进行绘制的。熟悉 WebGL 的应该了解,不论多么复杂的图形或模型,最终都是通过将其分解为若干三角形组成,WebGL 地图也是这样实现的,这个过程称为 Triangulation,即三角形化。然而,在图形元素过多、数据量较大的情况下,分解过程会比较耗时,在 JavaScript 单线程渲染的时候可能会阻塞页面的其他行为,因此 WebGL 地图引擎采用 Web Worker 处理数据。
鉴于 Web Worker 在图形渲染上的妙用,接下来我们用一个 canvas 绘制的例子来直观看一下使用 Web Worker 渲染和主线程直接渲染 canvas 的性能差异,该处用到了 OffscreenCanvas;到目前为止,Canvas 的绘制功能都与 <canvas> 标签绑定在一起,这意味着 Canvas API 和 DOM 是耦合的。而 OffscreenCanvas,正如它的名字一样,通过将 Canvas 移出屏幕来解耦了 DOM 和 Canvas API。由于这种解耦,OffscreenCanvas 的渲染与 DOM 完全分离了开来,并且比普通 Canvas 速度提升了一些,而这只是因为两者(Canvas和DOM)之间没有同步。但更重要的是,将两者分离后,Canvas 将可以在 Web Worker 中使用,即使在 Web Worker 中没有 DOM。这给 Canvas 提供了更多的可能性。
项目结构:

// app.js
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const serve = require('koa-static');
app.use(serve(__dirname))
const fs = require('fs');
app.use(async (ctx, next) => {
ctx.type = 'text/html';
ctx.body = fs.createReadStream('./OffscreenCanvas.html');
});
app.listen(8000);<!-- OffscreenCanvas.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Offscreen Canvas In Web Workers</title>
<link rel="stylesheet" href="./OffscreenCanvas_files/style.css">
</head>
<body>
<header class="hide-in-iframe">
<h1>
Offscreen Canvas In Web Workers
</h1>
<div class="desc">
离屏 Canvas 允许在屏幕外创建 canvas ,并且也可以用在 web workers 中
</div>
</header>
<main class="supported">
<section>
<p class="hide-in-iframe">
OffscreenCanvas 可以避免由于主线程阻塞引起的动画掉帧
</p>
<p class="desc">
当您点击 "make me busy" 按钮时, 主线程画布上的动画被阻塞,而工作在 worker 线程中的动画仍然可以平稳播放
</p>
<button id="make-busy">Make me busy!</button>
<div id="busy"> </div>
<div class="display">
<div>
<h1>
主线程的 Canvas
</h1>
<canvas id="canvas-window" width="400" height="200"></canvas>
</div>
<div>
<h1>
worker 中的 Canvas
</h1>
<canvas id="canvas-worker" width="400" height="200"></canvas>
</div>
</div>
</section>
</main>
<script src="./OffscreenCanvas_files/animation.js"></script>
<!--
这里是以 Blob 方式引入 worker 线程的方法
-->
<!-- <script type="script/worker" id="workerCode">
let animationWorker = null;
self.onmessage = function(e) {
switch (e.data.msg) {
case 'start':
if (!animationWorker) {
importScripts(e.data.origin + 'OffscreenCanvas_files/animation.js');
animationWorker = new Animation(e.data.canvas.getContext('2d'));
}
animationWorker.start();
break;
case 'stop':
animationWorker.stop();
break;
}
};
</script> -->
<script>
(() => {
document.querySelector('#make-busy').addEventListener('click', () => {
document.querySelector('#busy').innerText = 'Main thread working...';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
Animation.fibonacci(40);
document.querySelector('#busy').innerText = 'Done!';
});
})
});
const animationWindow = new Animation(document.querySelector('#canvas-window').getContext('2d'));
animationWindow.start();
const worker = new Worker('OffscreenCanvas_files/worker.js')
// 由于 web worker 无法以本地 file 协议加载文件,因此也可以以 Blob 的形式加载 worker 代码:
// const workerCode = document.querySelector('#workerCode').textContent;
// const blob = new Blob([workerCode], { type: 'text/javascript' });
// const url = URL.createObjectURL(blob);
// const worker = new Worker(url);
const offscreen = document.querySelector('#canvas-worker').transferControlToOffscreen();
const urlParts = location.href.split('/');
if (urlParts[urlParts.length - 1].indexOf('.') !== -1) {
urlParts.pop();
}
worker.postMessage({ msg: 'start', origin: urlParts.join('/'), canvas: offscreen }, [offscreen]);
// URL.revokeObjectURL(url); // cleanup
})();
</script>
</body>
</html>// worker.js
let animationWorker = null;
self.onmessage = function(e) {
// console.log(e.data);
switch (e.data.msg) {
case 'start':
if (!animationWorker) {
importScripts(e.data.origin + 'OffscreenCanvas_files/animation.js');
animationWorker = new Animation(e.data.canvas.getContext('2d'));
}
animationWorker.start();
break;
case 'stop':
animationWorker.stop();
break;
}
};// animation.js
class Animation {
constructor(ctx) {
this.ctx = ctx;
this.x = ctx.canvas.width / 2;
this.y = ctx.canvas.height / 2;
this.rMax = Math.min(this.x - 20, this.y - 20, 60);
this.r = 40;
this.grow = true;
this.run = true;
this.boundAnimate = this.animate.bind(this);
}
static fibonacci(num) {
return (num <= 1) ? 1 : Animation.fibonacci(num - 1) + Animation.fibonacci(num - 2);
}
drawCircle() {
this.ctx.beginPath();
this.ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
this.ctx.fill();
};
animate() {
if (!this.run) {
return;
}
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
if (this.r === this.rMax || this.r === 0) {
this.grow = !this.grow;
};
this.r = this.grow ? this.r + 1 : this.r - 1;
this.drawCircle();
requestAnimationFrame(this.boundAnimate);
}
stop() {
this.run = false;
}
start() {
this.run = true;
this.animate();
}
}好了,展示 Canvas 动效的 demo 已经完成,在根目录执行 npm start 把项目跑起来看看吧,在 localhost:8000 访问项目:

在该 demo 中,左下角窗口展示的是主线程的 Canvas 动画,右下角展示的是 Worker 线程的 Canvas 动画;当我们点击“MAKE ME BUSY”按钮时,主线程会执行一次斐波那契数列运算:
// animation.js
static fibonacci(num) {
return (num <= 1) ? 1 : Animation.fibonacci(num - 1) + Animation.fibonacci(num - 2);
}
// OffscreenCanvas.html
document.querySelector('#make-busy').addEventListener('click', () => {
document.querySelector('#busy').innerText = 'Main thread working...';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
Animation.fibonacci(40);
document.querySelector('#busy').innerText = 'Done!';
});
})
});该递归运算会占用主线程较多的资源,从而短暂地阻塞主线程后续队列里的任务,导致主线程动画会在斐波那契数列运算的过程中卡住,而与此同时 Worker 线程中的动画则可以流畅运行,丝毫不受到主线程阻塞的影响;由此我们不难看出,当页面需要渲染动画,但主线程上有可能执行一些消耗内容比较大的任务时,将动画绘制逻辑放在 Web Worker 中执行,然后将结果返回主线程,这样可以大大提高动画的渲染性能。
Web Worker 在使用中,有以下几个需要注意的点:
Web Worker 无法加载本地文件,但是假如我们没有掌握nodejs技术,或者实在懒得把项目放在服务器上,只想单纯地在本地调试 Web Worker,该怎么实现呢?本文提供一种解决方式:通过 Blob() 方式创建,具体步骤如下:
<!-- 这里是以 Blob 方式引入 worker 线程的方法 -->
<script type="script/worker" id="workerCode">
let animationWorker = null;
self.onmessage = function(e) {
switch (e.data.msg) {
case 'start':
if (!animationWorker) {
importScripts(e.data.origin + 'OffscreenCanvas_files/animation.js');
animationWorker = new Animation(e.data.canvas.getContext('2d'));
}
animationWorker.start();
break;
case 'stop':
animationWorker.stop();
break;
}
};
</script>// 由于 web worker 无法以本地 file 协议加载文件,因此也可以以 Blob 的形式加载 worker 代码:
const workerCode = document.querySelector('#workerCode').textContent;
const blob = new Blob([workerCode], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);这样即可避免 Worker 直接从本地以 file:// 的形式加载脚本,是不是很方便呢~
怎么样,Web Worker 的功能还是很强大的吧,如果您的项目中有需要前端执行大量运算或者绘制 Canvas、WebGL 等图形的 case,不妨将它们迁移在 Web Worker 中试试。
Web Worker 文献综述(全):http://km.oa.com/group/38989/articles/show/428931?kmref=search&from_page=1&no=1
Web Workers API:https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
Web Workers API:https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
OffscreenCanvas:https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。