JavaScript中并行性的快速介绍
并行主义在JavaScript中?你可能会想,我们已经在使用setTimeout() 、setInterval() 、XMLHttpRequest 、async/await 和事件处理程序在JavaScript中进行并行编程了。但这并不是真的。作为开发者,我们一直在模仿并行编程,因为JavaScript的single threading nature 使用事件循环。
是的,以上所有的技术都是异步的和非阻塞的。但是,这并不一定意味着并行编程。JavaScript的异步事件是在当前执行的脚本屈服后处理的。
满足 "网络工作者 "的要求
WebWorker为JavaScript引入了真正的并行编程。
Web Worker允许你启动长期运行的计算成本较高的任务,这有助于已经很拥挤的主线程将所有时间都用于布局和绘画😃。
理解渲染器流程中的Web Worker
Web Worker在渲染器进程中的位置(chrome中的一个进程,它负责标签内发生的一切)。

每个chrome标签都有自己的渲染器进程,chrome也会尝试为具有相同域的标签提供一个渲染器进程,如果你想知道同一个渲染器进程是如何共享一个v8实例的,祝你好运!
渲染器进程会创建多个线程:
- 主线程:处理大部分发送到浏览器的JavaScript
- 工作线程:处理Web工作者
- 合成器和光栅线程:负责渲染页面的流畅和高效
为了好玩,你可以通过以下方式检查chrome中运行的进程(不仅仅是渲染器进程)。
Menu(3 verticle dots on top right) > More tools > Task Manager
如何使用?
所以,只要我们做了
const worker = new Worker("worker.js")
就会创建新的工作线程,我们在worker.js 中的代码就会在其中运行。
与 Worker 进行通信
你不能直接调用工人线程中的函数。主线程和工作者之间的通信是通过via sending and listening to message event 完成的。
示例。寻找一个字符串的长度。
在index.js
// message event helps to communicate between threads.
// to listen for incoming messages from the worker.
worker.addEventListener('message', e => {
console.log(`Message from worker ${e.data}`)
})
// to send message to the worker
worker.postMessage("Hello from main")
和
中worker.js
// to listen for incoming messages from the main thread or other worker.
// Yes, a worker can also create subworkers.
self.addEventListener('message', e => {
console.log(`Message from main thread ${e.data}`)
// send a message back to the main thread
self.postMessage(e.data.length)
})
这里,postMessage需要一个参数message ,这个参数将在交付的事件的数据键中可用,消息的值应该由结构化克隆算法支持。
你也可以使用第二个参数来发送可转移对象的数组:
worker.postMessage(message, [transfer]);
工作者范围
Worker 的self 和this 都引用了 Worker 的全局范围。
根据前面的例子,self.postMessages(e.data.length) 也可以写成postMessages(e.data.length)
限制
由于工作者线程的多线程行为和线程安全,它不具备主线程所具有的一些功能:
- DOM
- 窗口对象
- 文档对象
- 父对象
参见MDN中的其他可用函数和类。
真实世界的例子
说得够多了,我们来写代码吧。
我们将编写一个小程序,使用Web工作者完成一些紧张的任务:接受一张图片并应用过滤器。
在终端运行
$ mkdir filter
$ touch index.html script.js worker.js
Html设置:
<!--index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Worker</title>
</head>
<body>
<!-- Input to select filter -->
<p>
<label>
Choose a filter to apply
<select id="filter">
<option value="none">none</option>
<option value="grayscale">grayscale</option>
<option value="brighten">brighten by 20%</option>
<option value="threshold">threshold</option>
</select>
</label>
</p>
<!-- Get image -->
<form accept-charset="utf-8" action="asd">
<label>Image to filter</label>
<input type="file" name="image" id="image-to-filter" alt="Enter image to filter">
</form>
<!-- show output here -->
<canvas id="output"></canvas>
<script src="script.js"></script>
</body>
</html>
脚本设置:
//script.js
document.addEventListener('DOMContentLoaded', () => {
const inputImage = document.getElementById('image-to-filter');
const output = document.getElementById('output');
const filter = document.querySelector('#filter');
// Context let you draw on the canvas
const outputContext = output.getContext('2d');
// This will return HTMLImageElement (<img>)
const img = new Image();
let imageData;
const drawImg = () => {
output.height = img.height;
output.width = img.width;
outputContext.drawImage(img, 0, 0);
// returns ImageData object, we can get pixel data from ImageData.data
// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data
imageData = outputContext.getImageData(0, 0, img.height, img.width);
}
// Getting image from the user
inputImage.addEventListener('change', e => {
// API to read files from the user
const reader = new FileReader();
// will be called once we read some file using any of the reader
// function available for FileReader
reader.onload = e => {
img.src = e.target.result;
img.style.display = 'none';
// will be called once the src is downloaded
img.onload = () => {
document.body.appendChild(img);
drawImg();
};
};
// reads the file as a dataURL
reader.readAsDataURL(e.target.files[0]);
});
})
好了,这是怎么回事呢?这主要是实现细节,即我们如何从用户那里获得图像,并将其值存储在imageData 。
这将创建一个工作者来运行worker.js 中的代码:
// script.js
const worker = new Worker('worker.js');
在worker.js 中设置一个工作者。
如果你像我一样,你的第一直觉是,让我们传递一个函数来回调给worker,让它结束,但是,你不能传递一个函数给web worker,因为结构化克隆算法不支持函数:
// worker.js
const Filters = {};
Filters.none = function none() {};
Filters.grayscale = ({data: d}) => {
for (let i = 0; i < d.length; i += 4) {
const [r, g, b] = [d[i], d[i + 1], d[i + 2]];
// CIE luminance for the RGB
// The human eye is bad at seeing red and blue, so we de-emphasize them.
d[i] = d[i + 1] = d[i + 2] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
};
Filters.brighten = ({data: d}) => {
for (let i = 0; i < d.length; ++i) {
d[i] *= 1.2;
}
};
Filters.threshold = ({data: d}) => {
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
var v = 0.2126 * r + 0.7152 * g + 0.0722 * b >= 90 ? 255 : 0;
d[i] = d[i + 1] = d[i + 2] = v;
}
};
onmessage = e => {
const {imageData, filter} = e.data;
Filters[filter](imageData);
postMessage(imageData);
};
- 我们正在监听一个消息事件
- 获取图像数据
- 筛选图像数据
- 将imageData发回给主线程。
向工作者发送一条消息:
// script.js
const filter = document.querySelector('#filter');
let imageData;
filter.addEventListener('change', e => sendImageDataToWorker())
const sendImageDataToWorker = () => {
worker.postMessage({imageData, filter: filter.value})
}
在这里,我们正在监听过滤器的变化,然后将 imageData 和要使用的过滤器发送给 Worker。
监听来自worker
//script.js
worker.onmessage = () => e => outputContext.putImageData(e.data, 0, 0);
我们正在监听来自工作者的消息,然后改变上下文中的图像数据。
最后,script.js ,将
// script.js
document.addEventListener('DOMContentLoaded', () => {
const worker = new Worker('filter_worker.js');
const inputImage = document.getElementById('image-to-filter');
const outputC = document.getElementById('output');
const filter = document.querySelector('#filter');
const oCtx = outputC.getContext('2d');
const img = new Image();
let imageData;
const sendDataToWorker = () =>
worker.postMessage({imageData, filter: filter.value});
const receiveFromWorker = e => oCtx.putImageData(e.data, 0, 0);
worker.onmessage = receiveFromWorker;
const drawImg = () => {
outputC.height = img.height;
outputC.width = img.width;
console.log(img.height);
oCtx.drawImage(img, 0, 0);
imageData = oCtx.getImageData(0, 0, img.height, img.width);
sendDataToWorker();
};
inputImage.addEventListener('change', e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = e => {
img.src = e.target.result;
img.style.display = 'none';
img.onload = () => {
document.body.appendChild(img);
drawImg();
};
};
reader.readAsDataURL(file);
});
filter.addEventListener('change', e => sendDataToWorker());
});
终止 Worker
您可以从主线程终止 Worker,方法是调用worker.terminate()
worker.terminate()
如果您想从工作者本身终止,可以调用工作者的close() 函数
close()
调用close() 时,事件循环中存在的任何排队任务都会被丢弃,Web Worker 的范围会被关闭。网络工作者也没有时间进行清理,因此突然终止工作者可能会导致内存泄漏。
其他网络工作者
在现实中,有两种类型的网络工作者。专用型和共享型,在本篇文章的范围内,我们只使用专用型工作者。
使用案例
- 与WebAssembly互动
- 语法高亮
- 图像过滤
- 音频/视频分析
- 处理大量的数组(对于数组,你可以使用二进制数组,这样复制起来更快)。