回到顶部
一: web workers 的基本原理
我们都知道, 我们的 JavaScript 采用的是单线程模型, 所有的任务都在一个主线程中完成, 一次只能执行一个任务, 如果有多个任务需要被执行的话, 那么后面的任务会依次排队等着, 那么这种情况下, 如果我们需要处理大量的计算逻辑的时候, 那么就会比较耗时, 那么用户界面就很有可能出现假死的状态, 或者浏览器被直接卡死, 这样非常影响用户体验. 这个时候我们的 Web workers 就出现了, 来解决这样类似的问题.
我们可以把 javascrpt 单线程模式理解我们日常生活中的快餐收银员可能会更好的理解, 我们平时吃快餐依次排队, 然后结账, 目前只有一个收银员结账, 所有的人都要排队依次来, 那假如说某个人拿了很多很多菜, 收营员需要慢慢的算账到底要收多少钱, 那么这个时候一般会要多点时间, 那么其他人就在后面排着队等着, 等前面的结账完成后, 再依次去结账, 这样就会很耗时, 那假如现在收银员有 2 个或更多的地方结账的话, 我们就可以到其他人少的地方去结账了, 这样就可以使速度更快的去完成某个任务, 其实现在我们的 Web workers 也是这么一种机制, 一些复杂业务逻辑, 我们的主线程可以把这些任务交给 Web workers 子线程去处理, 子线程处理完成后, 会把结果返回给主线程, 然后我们的主线程就执行.
Web workers 的作用: 它使用 JavaScript 创建 workers 线程, 我们浏览器主线程可以把一些复杂的业务处理逻辑交给 worker 线程去运行, 在我们的主线程运行的同时, 我们的 worker 线程也在后台运行, 两者互补干扰. 等到 worker 线程完成计算任务的时候, 会再把结果返回给主线程. 这样的优点是: 一些复杂的计算逻辑, 我们可以把它交给 worker 线程去完成, 主线程就会很流畅, 不会被阻塞了.
Worker 线程一旦创建成功了, 就会始终运行了, 不会被主线程的运行打断, 虽然这样有利于随时响应主线程的通信, 但是这也造成了 Worker 比较耗费资源, 在我们编码过程中, 可以适当的使用, 并且如果使用完成后, 我们应该需要关闭.
Web Worker 使用注意如下常见的几点:
1. 同源限制: 分配给 Worker 线程运行的脚本文件, 必须与主线程的脚本文件同源.
2. DOM 限制: Worker 所在的线程它不能操作 Windows,document 这样的对象, 也就是说 worker 线程不能操作 dom 对象
, 但是 worker 线程可以操作业务逻辑, 然后把操作后的结果返回给主线程, 主线程再去做相关的 DOM 操作.
3. 文件限制: Worker 线程无法读取本地文件, 也就是说不能打开本机的文件系统 (如: file://) 这样的, 它所加载的脚本, 必须来自网络.
Web Worker 浏览器支持程度如下所示:
回到顶部
二: Web Workers 的基本用法
1. 创建 worker 线程方法:
我们在主线程 JS 中调用 new 命令, 然后实列化 Worker() 构造函数, 就可以创建一个 Worker 线程了, 如下代码所示:
var worker = new Worker('work.js');
Worker() 构造函数的参数是一个脚本文件, 该文件就是 Worker 线程需要执行的任务, 由于 Worker 不能读取本地文件, 所以这个脚本必须来自网络.
如果我们在本地调用 work.JS 线程的话, 就会报如下错
如上初始化完成后, 我们的主线程需要向子线程发送消息, 使用 worker.postMessage() 方法, 向 Worker 发送消息. 如下所示:
worker.postMessage('hello world');
worker.postMessage 方法可以接受任何类型的参数, 甚至包括二进制数据.
发送消息完成后, 子线程去处理操作, 然后把结果返回给主线程, 那么主线程通过 worker.onmessage 指定监听函数, 接收子线程传送回来的消息, 如下代码所示:
- worker.onmessage = function(event) {
- console.log('接收到的消息为:' + event.data);
- }
如上代码, 事件对象的 data 属性可以获取 worker 发送回来的消息.
如果我们的 worker 线程任务完成后, 我们的主线程可以把它关闭掉, 使用如下代码:
worker.terminate();
2. worker 线程
Worker 线程内部需要有一个监听函数, 监听主线程 / 其他子线程 发送过来的消息. 监听事件为'message'. 如下代码所示:
- self.addEventListener('message', function(e) {
- self.postMessage('子线程向主线程发送消息:' + e.data);
- self.close(); // 关闭自身
- });
如上代码, self 代表子线程本身, 也可以为子线程的全局对象.
注意: 主线程向子线程发送消息为: worker.postMessage('hello world'); 这样的, 但是子线程向主线程发送消息, 是如下代码所示:
self.postMessage('子线程向主线程发送消息:' + e.data);
其实上面的写法 和如下两种写法是等价的, 如下代码:
- // 写法一
- this.addEventListener('message', function(e) {
- this.postMessage('子线程向主线程发送消息:' + e.data);
- this.close(); // 关闭自身
- });
- // 写法二
- addEventListener('message', function(e) {
- postMessage('子线程向主线程发送消息:' + e.data);
- close(); // 关闭自身
- });
注意: 如果我们使用了 self.addEventListener 来监听函数的话, 那么我们也要使用 self.postMessage() 这样的来发送消息, 如果我们使用 this.addEventListener 来监听函数的话, 那么也应该使用 this.postMessage 来发送消息, 如果我们使用如下方法: addEventListener('message', function(e) {}); 来监听函数的话, 那么我们就可以使用 postMessage() 方法来发送消息的.
3. 了解 importScripts() 方法
如果我们的 worker 内部需要加载其他的脚本的话, 我们可以使用 importScripts() 方法. 如下代码所示:
importScripts('a.js');
当然该方法也可以加载多个脚本, 如下代码所示:
importScripts('a.js', 'b.js');
4. 错误处理
主线程可以监听 Worker 线程是否发生错误, 如果发生错误, Worker 线程会触发主线程的 error 事件.
- // 方法一
- worker.onerror(function(e) {
- console.log(e);
- });
- // 方法二
- worker.addEventListener('error', function(e) {
- console.log(e);
- });
worker 线程内部也是可以监听 error 事件的.
5. 关闭线程
如果我们的线程使用完毕后, 为了节省系统资源, 我们需要关闭线程. 如下方法:
- // 关闭主线程
- worker.terminate();
- // 关闭子线程
- self.close();
回到顶部
三: 在 webpack 中配置 Web Workers
还是和之前一样, 配置之前, 我们来看下我们项目整个目录架构如下:
|--- Web-worker 项目
1. 在项目中安装 worker-loader 依赖, 如下命令所示:
NPM install -D worker-loader
2. 在 webpack 配置中添加如下配置:
- module.exports = {
- module: {
- rules: [
- {
- test: /\.worker\.JS$/, // 以 .worker.JS 结尾的文件将被 worker-loader 加载
- use: {
- loader: 'worker-loader',
- options: {
- inline: true
- // fallback: false
- }
- }
- }
- ]
- }
- }
如上正则匹配的是以 以 .worker.JS 结尾的文件将被 worker-loader 加载, 也就是说我们在项目中我们的 worker 文件名可以叫 test.worker.JS 类似这样的名字, 或其他的, 只要保证 xxx.worker.JS 这样的文件名即可.
在上面配置中, 设置 inline 属性为 true 将 worker 作为 blob 进行内联; 内联模式将额外为浏览器创建 chunk, 即使对于不支持内联 worker 的浏览器也是这样的; 比如如下运行, 我们可以在我们的本地项目下看到有如下这么一个请求:
在开发环境下或正式环境中 我们要如何配置呢?
如果在本地开发中, 我们会使用 webpack-dev-server 启动本地调式服务器, 如果只有上面的配置的话, 我们可以在控制台中会报如下的错误;"Uncaught ReferenceError: window is not defined"; 这样的错误, 如下所示:
要解决如上的错误的话, 我们需要在我们的 webpack 配置文件下的 out 下, 加一个属性 globalObject: 'this'; 如下代码:
- module.exports = {
- output: {
- globalObject: 'this'
- }
- }
比如我现在的 webpack 配置如下:
- module.exports = {
- output: {
- filename: process.env.NODE_ENV === 'production' ? '[name].[contenthash].js' : '[name].js',
- // 将输出的文件都放在 dist 目录下
- path: path.resolve(__dirname, 'dist'),
- publicPath: '/',
- globalObject: 'this'
- }
- }
然后我们继续运行下 就没有报错了.
首先来看下我们的 public/JS/main.JS 代码如下:
- // 加载 CSS 样式
- require('../styles/main.styl');
- import Worker from './test1.worker.js';
- // 创建 worker 实列
- var worker = new Worker();
- // 向 worker 线程发送消息
- worker.postMessage('主线程向 worker 线程发送消息');
- // 监听 worker 线程发送回来的消息
- worker.onmessage = function(e) {
- console.log('监听 worker 线程发送回来的消息如下所示:')
- console.log(e);
- };
然后我们的 public/JS/test1.worker.JS(子线程) 的代码如下所示:
- // 监听消息
- onmessage = function(e) {
- console.log('监听到的消息为:' + e.data);
- }
- const msg = '工作线程向主线程发送消息';
- // 发送消息
- postMessage(msg);
然后运行结果如下所示:
如上代码, 我们首先创建了一个 worker 实列, 如代码: var worker = new Worker(); 然后他就会调用 test1.worker.JS 代码, 该 worker 中的代码会首先给主线程发送消息, 消息文本为: '工作线程向主线程发送消息'; 然后我们的主线程中会通过 worker.onmessage 事件来监听子线程的消息, 因此我们第一次打印出来为如下代码的消息:
- worker.onmessage = function(e) {
- console.log('监听 worker 线程发送回来的消息如下所示:')
- console.log(e);
- };
然后我们的主线程才会向子线程发送消息, 如下代码:
- // 向 worker 线程发送消息
- worker.postMessage('主线程向 worker 线程发送消息');
然后 test1.worker.JS 代码中的 onmessage 就能监听到消息, 如下所示:
- // 监听消息
- onmessage = function(e) {
- console.log('监听到的消息为:' + e.data);
- }
最后就会打印出信息如下:"监听到的消息为: 主线程向 worker 线程发送消息".
回到顶部
四: Web Worker 的应用场景
4.1. 使用 Web workers 来解决耗时较长的问题
现在我们需要做一个这样的 demo, 我们在页面中有一个 input 输入框, 用户需要在该输入框中输入数字, 然后点击旁边的计算按钮, 在后台计算从 1 到给定数值的总和. 如果我们不使用 Web workers 来解决该问题的话, 如下 demo 代码所示:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- Web worker 实列
- </title>
- </head>
- <body>
- <h1>
- 从 1 到给定数值的求和
- </h1>
- 输入数值:
- <input type="text" id="num" />
- <button onclick="calculate()">
- 计算
- </button>
- <script type="text/javascript">
- function calculate() {
- var num = parseInt(document.getElementById("num").value, 10);
- var result = 0;
- // 循环计算求和
- for (var i = 0; i <= num; i++) {
- result += i;
- }
- alert('总和魏:' + result + '.');
- }
- </script>
- </body>
- </HTML>
如上代码, 然后我们输入 1 百亿, 然后让计算机去帮我们计算, 计算的时间应该要 20 秒左右的时间, 但是在这 20 秒之前的时间, 那么我们的页面就处于卡顿的状态, 也就是说什么都不能做, 等计算结果出来后, 我们就会看到如下弹窗提示结果了, 如下所示:
那现在我们需要使用我们的 Web workers 来解决该问题, 我们希望把这些耗时操作使用 workers 去解决, 那么主线程就不影响页面假死的状态了, 我们首先把 index.HTML 代码改成如下:
- <!DOCTYPE HTML>
- <HTML lang="en">
- <head>
- <meta charset="UTF-8">
- <title>
- Web worker 实列
- </title>
- </head>
- <body>
- <h1>
- 从 1 到给定数值的求和
- </h1>
- 输入数值:
- <input type="text" id="num" />
- <button id="calculate">
- 计算
- </button>
- </body>
- </HTML>
然后在我们的 public/JS/main.JS 代码如下:
- // 加载 CSS 样式
- require('../styles/main.styl');
- import Worker from './test1.worker.js';
- // 创建 worker 实列
- var worker = new Worker();
- var calDOM = document.getElementById('calculate');
- calDOM.addEventListener('click', calculate);
- function calculate() {
- var num = parseInt(document.getElementById("num").value, 10);
- // 将我们的数据传递给 worker 线程, 让我们的 worker 线程去帮我们做这件事
- worker.postMessage(num);
- }
- // 监听 worker 线程的结果
- worker.onmessage = function(e) {
- alert('总和值为:' + e.data);
- };
public/JS/test1.worker.JS 代码如下:
- // 监听消息
- onmessage = function(e) {
- var num = e.data;
- var result = 0;
- for (var i = 0; i <= num; i++) {
- result += i;
- }
- // 把结果发送给主线程
- postMessage(result);
- }
如上代码我们运行下可以看到, 我们点击下计算按钮后, 我们使用主线程把该复杂的耗时操作给子线程处理后, 我们点击按钮后, 我们的页面就可以操作了, 因为主线程和 worker 线程是两个不同的环境, worker 线程的不会影响主线程的. 因此如果我们需要处理一些耗时操作的话, 我们可以使用 Web workers 线程去处理该问题.
4.2. 实现创建内嵌的 worker
如上是在 webpack 中配置使用 Web workers 的使用, 我们也可以实现创建内嵌的 worker, 那么什么是 内嵌的 worker 呢? 首先我们把 webpack 中的如下配置代码注释掉:
- module.exports = {
- output: {
- // globalObject: 'this'
- }
- }
然后我们运行代码, 肯定报错:'Uncaught ReferenceError: window is not defined'. 那么现在我们使用 创建内嵌的 worker 来解决这样的问题. 我们通过 URL.createObjectURL() 创建 URL 对象, 可以实现创建内嵌的 worker. 我们把上面的 test1.worker.JS 代码写到一个 JS 文件里面, 也就是写到 main.JS 里面去, 如下代码:
- var myTask = `onmessage = function(e) {
- var num = e.data;
- var result = 0;
- for (var i = 0; i <= num; i++) {
- result += i;
- }
- // 把结果发送给主线程
- postMessage(result);
- }`;
- var blob = new Blob([myTask]);
- var myWorker = new Worker(Windows.URL.createObjectURL(blob));
- // 创建 worker 实列
- // var worker = new Worker();
- var calDOM = document.getElementById('calculate');
- calDOM.addEventListener('click', calculate);
- function calculate() {
- var num = parseInt(document.getElementById("num").value, 10);
- // 将我们的数据传递给 worker 线程, 让我们的 worker 线程去帮我们做这件事
- myWorker.postMessage(num);
- }
- // 监听 worker 线程的结果
- myWorker.onmessage = function(e) {
- alert('总和值为:' + e.data);
- };
注意: 这边只是简单的演示下 Web worker 能解决一些耗时操作的问题, 如果想要学习更多关于 Web workers 可以自己 google 下折腾下. 我这边先到此了. 也就是说, 如果在一些 JS 耗时的代码, 我们可以使用子线程来解决类似的问题, 这样就不会导致页面被卡死的状态.
Web-worker 项目 GitHub 查看 https://github.com/tugenhua0707/web-workers-demo (注意: 这只是一个框架, 内部没有任何代码, 我们可以把上面的代码复制到里面去运行下即可).
来源: https://www.cnblogs.com/tugenhua0707/p/11253937.html