
我们总说JS是单线程的,根据上一文我们也了解了JS引擎线程.
单线程意味着不能把工作委托给独立的线程或进程去做。JS的单线程可以保证它与不同的浏览器API兼容。如果JS可以多线程执行并发更改,那么像DOM这样的API就会出现问题。
这正是我们本文讨论的工作者线程的价值所在:允许把主线程的工作转嫁给独立的实体,而不会改变现有的单线程模型。
工作者线程简介
JS环境实际上托管在操作系统中的虚拟环境。使用工作者线程,浏览器可以在原始界面环境之外再分配一个完全独立的二级子环境。
这个子环境不能与依赖单线程交互的API(如DOM)互操作,但可以与父环境并行执行代码
工作者线程与线程
- 工作者线程是以实际线程实现的
- 工作者线程并行执行但不一定再同一个进程里
- 工作者线程可以共享某些内存,但不共享全部内存
- 创建工作者线程的开销更大
工作者线程类型
web工作者线程规范中定义了三种主要的工作者线程:专用工作者线程、共享工作者线程、服务工作者线程
专用工作者线程
通常简称为工作者线程、Web Worker或Worker.只能被创建它的页面使用
共享工作者线程
共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送给消息或从中接受消息
服务工作者线程
主要用途是拦截、重定向和修改页面发出的请求,充当网络请求仲裁者的角色。
WorkerGlobalScope
在网页中,window对象可以向运行在其中的脚本暴露各种全局属性。在工作者线程内部,没有window的概念。这里的全局对象是WorkerGlobalScope的实例,通过self关键字暴露出来。
WorkerGlobalScope属性和方法
self上可用的属性和方法是window对象上属性的严格子集。其中有些属性或方法会返回特定于工作者线程的版本。
WorkerGlobalScope的子类
每种类型的工作者线程都使用了自己特定的全局对象,继承关系如下
EventTarget->WorkerGlobalScope->子类
- 专用工作者线程使用DedicatedWorkerGlobalScope
- 共享工作者线程使用SharedWorkerGlobalScope
- 服务工作者线程使用ServiceWorkerGlobalScope
self 属性返回每个内容的专门 scope .
专用工作者线程
这样的线程可以与父页面进行交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现是其他不适合在页面执行线程的任务(防止阻塞页面的渲染)
使用工作者线程时,脚本在哪里执行,在哪里加载是非常重要的概念。
基本概念
可以称专用者线程为后台脚本。JS线程的各个方面,包括生命周期管理、代码路径和输入输出,都由初始化线程时提供的脚本来控制。
创建工作者线程
最常见的方式是加载JS文件。将文件路径提供给Worker构造函数,然后构造函数再在后台异步加载并实例化工作者线程。
const myWorker = new Worker(aURL, options);
假设在main.js文件中创建工作者线程,加载worker.js文件
worker文件是在后台加载的,工作者线程的初始化完全独立于main.js
工作者线程本身存在于独立的JavaScript环境中,因此main.js必须以Worker对象为代理实现与工作者线程通信。
安全限制
工作者线程的脚本必须遵守同源策略
使用Worker对象
Worker对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传递信息,以及捕获专用工作者线程发出的事件。
在终止工作者线程之前,Worker对象不会被垃圾回收,也不能通过编程的方式恢复对之前Worker对象的引用。
DedicatedWorkerGlobalScope
在专用工作者内部,全局作用域是DedicatedWorkerGlobalScope的实例,工作者线程通过self关键字访问该全局作用域。
因为工作者线程具有不可忽略的启动延迟,所以即使Worker对象存在,工作者线程的日志也会在主线程的日志之后打印出来
隐式MessagePorts
专用工作者线程隐式使用了Message在两个上下文之间通信
在工作者线程内部调用close()(或在外部调用terminate())不仅会关闭MessagePort,也会终止线程
生命周期
专用工作者线程可以区分为下列三个状态:初始化、活动和终止。
无法通过Worker对象确定工作者线程当前是处理初始化、活动还是终止状态。
内部终止
调用close()后,工作者线程的执行并没有立即停止,close()会通知工作者线程取消事件循环中的所有任务,并阻止立即添加新任务。
const workerScript = `
self.postMessage('foo')
self.close()
self.postMessage('bar')
setTimeOut(()=>self.postMessage('baz'),0)
`;
const workerScriptBlob = new Blob([workerScript]);
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
const worker = new Worker(workerScriptBlobUrl);
worker.onmessage=({data}) =>console.log(data)
console.log(worker);
// Worker
// foo
// bar
因为延迟的问题所以先打印Worker对象,
注意:在这里工作者线程不执行同步停止,所以bar也被打印出来
因为有些浏览器不支持本地通过文件路径形式创建工作者线程,所以本文均采用行内创建的方式做例
外部终止
const workerScript = `
self.onmessage=({data}) =>console.log(data)
`;
const workerScriptBlob = new Blob([workerScript]);
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
const worker = new Worker(workerScriptBlobUrl);
setTimeout(()=>{
worker.postMessage('foo')
worker.terminate()
worker.postMessage('bar')
setTimeout(()=>worker.postMessage('baz'),0)
},1000)
worker.onmessage=({data}) =>console.log(data)
console.log(worker);
// Worker对象
// foo
一旦调用terminate(),工作者线程的消息队列就会被清理并锁住,这也是只打印foo的原因
close()和terminate()这两个方法是幂等操作,仅仅是将Worker标记为teardown,因此多次调用不会有不好的影响。
在整个生命周期中,一个专用工作者线程只会关联一个网页(文档)。除非明确终止,否则只要关联文档存在,专用者线程就会存在。
如果浏览器离开网页(通过导航关闭标签页,或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会停止。
配置Worker选项
- name:在工作者线程中通过self.name获取字符串标识符
- type: 表示加载脚本的运行方式
- credentials:当type为module时,指定如何获取与传输凭证数据相关的工作者线程模块脚本。
在JS行内创建工作者线程
专用工作者线程也可以通过Blob对象URL在行内脚本创建,这样可以更快的初始化工作者线程,因为没有网络延迟。
const worker = new Worker(
URL.createObjectURL(
new Blob([`self.addEventListener('message',({data})=>console.log(data))`])
)
);
worker.postMessage("nihao");
工作者线程可以利用函数序列化来初始化行内脚本,因为函数的toString()方法返回函数代码的字符串,而函数可以在父上下文中定义但在子上下文中执行
序列化
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
function fibonacci(n) {
return n < 1 ? 0 : n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}
const workerScript = `self.postMessage((${fibonacci.toString()})(4))`;
const worker = new Worker(URL.createObjectURL(new Blob([workerScript])));
worker.onmessage = ({ data }) => console.log(data);
虽然递归计算斐波那契数列比较耗时,但所有的计算都会委托到工作者线程,因此并不会影响父上下文的性能
不过像这样序列化函数有个前提,就是函数体内不能使用通过闭包获得的引用,也包括全局变量,比如window,因为这些引用在工作者线程中执行会报错
在工作者线程中动态执行脚本
在工作者线程中可以使用importScripts()方法通过编程方式加载和执行任意脚本。
该方法可用于全局Worker对象,可接受任意数量的脚本作为参数,浏览器下宅他们没有限制,但执行会严格按照他们在参数列表的顺序进行。
所有导入的脚本也会共享作用域
委托任务到子线程
工作者线程中可以再创建子工作者线程,顶级工作者线程的脚本和子工作者线程的脚本都必须从与主页相同的源加载。
处理工作者线程错误
如果工作者线程脚本抛出了错误,该工作者线程沙盒可以阻止它打断父线程的执行。
不过相应的错误事件仍会冒泡到工作者线程上下文,因此可以通过在Worker对象上设置错误事件侦听器访问到。
与专用工作者线程之间的通信
postMessage()
MessageChannel()
- 端口
BroadcastChannel()
同源脚本能够通过BroadcastChannel相互之间发送和接受消息,这种信道没有端口所有权的概念,如果没有实时监听这个信道,广播的消息就不会有人处理。
太多了,后续再补上