WebWorker:工作者线程初探
WebWorker:工作者线程初探
参考资料:
1.Web Worker 使用教程 - 阮一峰:http://www.ruanyifeng.com/blog/2018/07/web-worker.html
2.JavaScript高级程序设计-第四版
一、概述
JavaScript 是单线程的,单线程就意味着不能像多线程语言那样把工作委托给独立的线程或进程去做,无法充分发挥现代计算机多核CPU的优势。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
web worker的一些特点:
- 同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。也就是说只能加载来自网络的脚本文件,无法读取本地文件
- DOM 限制:Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用
document
、window
、parent
这些对象,自然也不能alert(),但是,Worker 线程可以navigator
对象和location
对象。 - 实际线程:工作者线程是以实际线程实现的。但不一定在同一个进程里(一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于)
- 并行执行:虽然页面和工作者线程都是单线程 JavaScript 环境,每个环境中的指令则可以并行执行
- 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成
二、工作者线程的类型
Web 工作者线程规范中定义了三种主要的工作者线程:专用工作者线程、共享工作者线程和服务工作者线程:
- 专用工作者线程:通常简称为工作者线程、Web Worker 或 Worker,,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务,只能被创建它的页面使用
- 共享工作者线程:共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息
- 服务工作者线程:主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色
全局对象WorkerGlobalScope:
在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有 window的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来。
// 在正常环境中
console.log('self',self); // -> 打印结果是Window对象
// 在worker环境中
const worker_code = () => {
// 向主线程发送一条消息
self.postMessage('worker loaded successfully...');
// 监听主线程发来的信息
self.onmessage = (e) => {
console.log('接收到的event:',e);
let result = `从主线程接收到的数据 ${e.data}`;
self.postMessage(result)
}
console.log('self',self);
}
从结果上来看,self 上可用的属性是 window 对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本,不同的环境中,self指向结果是不一样的,
在主线程环境中,self指向window对象。在工作者线程中,self指向具体的WorkerGlobalScope的实例,比如在专用工作者线程中,self的指向的具体实例对象是
DedicatedWorkerGlobalScope。
WorkerGlobalScope 的子类:
实际上并不是所有地方都实现了 WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这继承自 WorkerGlobalScope
- 专用工作者线程使用 DedicatedWorkerGlobalScope
- 共享工作者线程使用 SharedWorkerGlobalScope
- 服务工作者线程使用 ServiceWorkerGlobalScope
三、用法示例
这里以专用工作者线程为例,看看他的用法:
创建一个worker-test.js,内容如下
const worker_code = () => {
// 向主线程发送一条消息
self.postMessage('worker loaded successfully...');
// 监听主线程发来的信息
self.onmessage = (e) => {
console.log('接收到的event:',e);
let result = `从主线程接收到的数据 ${e.data}`;
self.postMessage(result)
}
console.log('self',self);
}
let code = worker_code.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
console.log(code);
const blob = new Blob([code],{ type: 'application/javascript' });
const worker_script = URL.createObjectURL(blob);
export default worker_script;
在主环境中创建Worker:
const worker = new Worker(worker_script, { name: 'myWorker' });
console.log(worker);
worker.onmessage = (event) => {
console.log('主线程接收到了消息:',event.data)
}
// 点击按钮向工作者线程发送消息
const handleClick = () => {
worker.postMessage('这是一条主线程发送的消息')
}
<button onclick="handleClick()">发送消息</button>
点击按钮并发送消息,完成主线程与工作者线程的通信,这样就完成了一个简单的通信例子。
worker构造函数接收两个参数,第一个参数是要加载的脚本内容,这个脚本既可以从url获取,也可以传入一个字符串格式的javascript代码,第二个参数则是配置信息,比如可以指定创建worker的名称。
使用模板字符串形式
上面的例子通过把function转换成string再由Blob创建工作者线程,另一种写法是,通过模板字符串直接写js代码
// 创建要执行的 JavaScript 代码字符串
const workerScript = `
self.onmessage = ({data}) => console.log(data);
`;
// 基于脚本字符串生成 Blob 对象
const workerScriptBlob = new Blob([workerScript]);
// 基于 Blob 实例创建对象 URL
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
// 基于对象 URL 创建专用工作者线程
const worker = new Worker(workerScriptBlobUrl);
worker.postMessage('blob worker script');
四、详细介绍
上面内容初步介绍了web worker的一些概念以及简单用法,下面将详细介绍工作者线程的内容(这里以专用工作者线程为例)
专用工作者线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务。
加载worker的限制:
工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误
const sameOriginWorker = new Worker('./worker.js'); // 尝试基于当前路径源加载,可行
const remoteOriginWorker = new Worker('https://untrusted.com/worker.js'); // 跨域加载,将会报错
work对象:
构建出来的worker对象拥有如下API:
方法 | 说明 |
---|---|
onerror | 监听在在工作者线程中发生 ErrorEvent 类型的错误事件,也可以通过 worker.addEventListener('error', handler)的形式处理 |
onmessage | 监听在工作者线程中发生 MessageEvent 类型的消息事件,例如工作者线程中使用postMessage发送消息 |
onmessageerror | 监听在工作者线程中发生 MessageEvent 类型的错误事件 |
postMessage | 发送消息(异步) |
terminate | 用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止 |
DedicatedWorkerGlobalScope实例:
在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例,继承WorkerGlobalScope,可以通过 self 关键字访问该全局作用域
DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法:
方法 | 说明 |
---|---|
name | 给worker起个名字 |
postMessage | 与之关联的上下文发送消息 |
close | 与worker.terminate()方法一样,立即终止工作者线程(先取消事件循环里的任务) |
importScripts | 用于向工作者线程中导入脚本,可以跨域 |
生命周期:
从const worker = new Worker()
开始,worker的生命周期就存在了,一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始(initializing)、活动(active) 和终止(terminated)。为什么是非正式的状态,因为其实这几个状态对其他上下文是不可见的,比如主线程环境中其实无法知道web worker的加载状态,也没有相关api来表明能够监听到具体的生命周期执行阶段。
const worker = new Worker('./initializingWorker.js');
// Worker 可能仍处于初始化状态
// 但 postMessage()数据可以正常处理
worker.postMessage('foo');
worker.postMessage('bar');
worker.postMessage('baz');
初始化时,虽然工作者线程脚本尚未执行(可能由于网络原因,initializingWorker还未传输完毕),但可以先把要发送给工作者线程的消息加入队列。这些消息会等待工作者线程的状态变为活动,再把消息添加到它的消息队列。
创建之后,专用工作者线程就会伴随页面的整个生命期而存在,除非自我终止(self.close())或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。只要工作者线程仍存在,与之关联的 Worker 对象就不会被当成垃圾收集掉。
终止生命周期:self.close()方法和worker.terminate()方法都会终结掉工作者线程,但是他们的行为有些差异
自我终止:
// closeWorker.js
self.postMessage('foo');
self.close();
self.postMessage('bar');
setTimeout(() => self.postMessage('baz'), 0);
// main.js
const worker = new Worker('./closeWorker.js');
worker.onmessage = ({data}) => console.log(data);
自我终止不会立即终止,close()在这里会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务,工作者线程不需要执行同步停止,因此在父上下文的事件循环中处理的"bar"仍会打印出来。
外部终止:
// terminateWorker.js
self.onmessage = ({data}) => console.log(data);
// main.js
const worker = new Worker('./terminateWorker.js');
// 给 1000 毫秒让工作者线程初始化
setTimeout(() => {
worker.postMessage('foo');
worker.terminate();
worker.postMessage('bar');
setTimeout(() => worker.postMessage('baz'), 0);
}, 1000);
// foo
这里,外部先给工作者线程发送了带"foo"的 postMessage,这条消息可以在外部终止之前处理。一旦调用了 terminate(),工作者线程的消息队列就会被清理并锁住,这也是只是打印"foo"的原因。
在整个生命周期中,一个专用工作者线程只会关联一个网页(Web 工作者线程规范称其为一个文档)。除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。
配置项:
new worker的第二个参数有如下配置项:
配置项 | 说明 |
---|---|
name | 工作者线程的名字 |
type | 表示加载脚本的运行方式,可以是"classic"或"module"。"classic"将脚本作为常规脚本来执行,"module"将脚本作为模块来执行。 |
credentials | 传输凭证:值可以是"omit"、"same-orign"或"include" |
使用importScripts导入其他脚本:
在工作者线程内部,可以使用importScripts导入任意数量的脚本,并且可以跨域
importScripts('./scriptA.js');
importScripts('./scriptB.js'); // 等同于 importScripts('./scriptA.js', './scriptB.js');
五、在React中使用web worker
很幸运,react-webworker-hook提供了hooks钩子函数来简化webworker的使用方式,代码示例如下:
首先安装工具包
npm install --save react-webworker-hook
使用它计算斐波那契数值:
import React, { useState } from "react";
import useWebWorker from "react-webworker-hook";
function GenerateFibonacci() {
const [data = 0, postData] = useWebWorker({
url: "./webworker_example.js"
});
const [count, setCount] = useState(0);
return (
<div>
{`fibonacci for ${count}: ${data}`}
<button
onClick={() => {
setCount(count + 1);
postData(count);
}}
>
Generate
</button>
</div>
);
}
export default GenerateFibonacci;
也可以通过useWebWorkerFromScript钩子函数直接书写字符串形式
import React, { useState } from "react";
import { useWebWorkerFromScript } from "react-webworker-hook";
function GenerateFibonacci() {
const [data = 0, postData] = useWebWorkerFromScript(`
const fib = n => (n < 2 ? n : fib(n - 1) + fib(n - 2));
onmessage = ({ data }) => {
postMessage(fib(data));
};
`);
const [count, setCount] = useState(0);
return (
<div>
{`fibonacci for ${count}: ${data}`}
<button
onClick={() => {
setCount(count + 1);
postData(count);
}}
>
计算
</button>
</div>
);
}
export default GenerateFibonacci;
六、不同类型的工作者线程对比
类型 | 通信方式 | 使用场景 |
---|---|---|
Worker | postMessage | 适合大量计算的场景,例如斐波那契计算 |
SharedWorker | port.postMessage | 适合跨 tab、iframes之间共享数据 |
ServiceWorker | 单向通信,通过 addEventListener 监听 serviceWorker 的状态 |
缓存资源、网络优化 |
以上就是Web Worker初探内容,实际上还有共享工作者线程和服务工作者线程还未说到。
WebWorker:工作者线程初探的更多相关文章
- Windows线程漫谈界面线程和工作者线程
每个系统都有线程,而线程的最重要的作用就是并行处理,提高软件的并发率.针对界面来说,还能提高界面的响应力. 线程分为界面线程和工作者线程,界面实际就是一个线程画出来的东西,这个线程维护一个“消息队列” ...
- MFC工作者线程
//************工作者线程**************1.在头文件中添加UINT ThreadFunc(LPVOID lpParam); 注意应在类的外部 2.添加protected型变量 ...
- UI线程和工作者线程
本文转载于:http://blog.csdn.net/libaineu2004/article/details/40398405 1.线程分为UI线程和工作者线程,UI线程有窗口,窗口自建了消息队列, ...
- C#线程学习笔记二:线程池中的工作者线程
本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/18/ThreadPool.html,记录一下学习过程以备后续查用. 一.线程池基础 首先,创 ...
- tomcat线程初探
博主:handsomecui,希望路过的各位大佬留下你们宝贵的意见,在这里祝大家冬至快乐. 缘由: 初探缘由,在业务层想要通过(当前线程的栈)来获取到控制层的类名,然后打日志,可是发现并不能通过当前线 ...
- 2016/1/2 Python中的多线程(1):线程初探
---恢复内容开始--- 新年第一篇,继续Python. 先来简单介绍线程和进程. 计算机刚开始发展的时候,程序都是从头到尾独占式地使用所有的内存和硬件资源,每个计算机只能同时跑一个程序.后来引进了一 ...
- 20181103_C#线程初探, BeginInvoke_EndInvoke
在C#中学习多线程之前, 必须要深刻的理解委托; 基本上所有的多线程都在靠委托来完成 一. 进程和线程: a) 进程和线程都是计算机的概念, 跟程序语言没有任何关系 b) 进程和线程都属于计算机操 ...
- LINUX线程初探
LINUX程序设计最重要的当然是进程与线程.本文主要以uart程序结合键盘输入控制uart的传输. 硬件平台:树莓派B+ 软件平台:raspberry 须要工具:USB转TTL(PL2303)+ ...
- C#多线程---线程池的工作者线程
一.线程池简介 创建和销毁线程是一个要耗费大量时间的过程,太多的线程也会浪费内存资源,所以通过Thread类来创建过多的线程反而有损于性能,为了改善这样的问题 ,.net中就引入了线程池. 线程池形象 ...
随机推荐
- 一个程序的自我修养「GitHub 热点速览 v.22.19」
一个程序要诞生涉及前后端技术,比如,你可以用可视化网页搭建工具 tmagic-editor 完成前端部分,而后端部分的数据库以及数据处理可能就要用到 jsonhero-web 和 directus.知 ...
- 最佳案例 | 游戏知几 AI 助手的云原生容器化之路
作者 张路,运营开发专家工程师,现负责游戏知几 AI 助手后台架构设计和优化工作. 游戏知几 随着业务不断的拓展,游戏知几AI智能问答机器人业务已经覆盖了自研游戏.二方.海外的多款游戏.游戏知几研发团 ...
- spring 配置文件 --bean
bean标配的基本配置 id:Bean实例在Spring容器中的唯一标识 class Bean的全限定名 scope 1.当scope的 ...
- 直观比较 popcount 的效率差异
问题 求 \(\sum\limits_{i=1}^{3\times 10^8} popcount(i)\) . 仅考虑在暴力做法下的效率. 枚举位 __builtin_popcount #includ ...
- 12.MYSQL基础-常见函数
4. 常见函数 一.字符函数 概念 类似于Java的方法,将一组逻辑语句封装在方法中,对外暴露方法名 优点 隐藏了实现细节 提高代码的重用性 调用 select 函数名(实参列表) [ from 表] ...
- mysql刷题笔记
近期,为提升自己的工程能力,在休息时常通过刷题来回顾一下基础性知识. 于是选择了牛客网上的mysql知识题库练手,过程中,主要遇到了几个比较有意思的题,记录下来,方便回顾. 题1:SQL29 计算用户 ...
- Linux 装完后没有声音的解决办法
备注:1)Ubuntu Desktop版本:16.042)Linux工作用户:root1. 临时方法在终端中执行命令:pulseaudio --start --log-target=syslog2. ...
- kruskal 及其应用
kruskal 最小生成树 kruskal 是一种常见且好理解的最小生成树(MST)算法. 前置知识 并查集和路径压缩 生成树 在有 n 的顶点的无向图中,取其中 n-1 条边相连,所得到的树即为生成 ...
- java中的方法重载(overload)
什么时候方法重载:当两个方法的功能是相似的,可以考虑使用方法重载.若两个方法根本没有关系,无必要使用方法重载. 什么时候代码会发生方法重载:三个条件:1,在同一个类中.2,方法名相同.3,参数列表相同 ...
- 名校AI课推荐 | UC Berkeley《人工智能导论》
深度学习具备强感知能力但缺乏一定的决策能力,强化学习具备决策能力但对感知问题束手无策,因此将两者结合起来可以达到优势互补的效果,为复杂系统的感知决策问题提供了解决思路. 今天我们推荐这样一门课程--U ...