2026-05-11 09:46:59
我们每个人来到这个世上,都有着自己注定要完成的使命;但是,我们不知道我们的使命是什么。所以,我们需要在前进的路上,不断地去反思,不停地去理解。只有当我们明白了我们的使命,明白了我们奋斗的意义,我们才不会在众多的岔路中迷失方向,才会走的更加坚定。--《心理学通识》
前言大家好,我是柒八九。
因为,最近有一个需求中,用到了Worker技术,然后经过一些调研和调试,成功的在项目中应用。虽然,有部分原因是出于技术尝鲜的角度才选择Worker进行性能优化。但是,看懂了,会用了,领悟了。这是不同的技术层面。
所以,打算做一个Worker科普和实际生产应用的文章。
那我们就闲话少叙,开车走起。
文章概要Worker线程简介
{专用工作线程|Dedicated?Worker}
专用工作线程+Webpack
{共享工作线程|SharedWorkers}
Worker线程简介JavaScript环境实际上是运行在托管操作系统(OS)中的虚拟环境
在浏览器中每打开一个页面,就会分配一个它自己的环境:即每个页面都有自己的内存、事件循环、DOM。并且每个页面就相当于一个沙盒,不会干扰其他页面。
而使用Worker线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的API(如DOM)互操作,但可以与父环境并行执行代码。
1.Worker线程vs线程共同之处工作者线程是以实际线程实现的:Blink浏览器引擎实现Worker线程的WorkerThread就对应着底层的线程
工作者线程并行执行:虽然页面和工作者线程都是单线程JS环境,每个环境中的指令则可以并行执行
工作者线程可以共享某些内存:工作者线程能够使用SharedArrayBuffer在多个环境间共享内容
区别worker线程不共享全部内存:除了SharedArrayBuffer外,从工作者线程进出的数据需要复制或转移
worker线程不一定在同一个进程里:例如,Chrome的Blink引擎对共享worker线程和服务worker线程使用独立的进程
创建worker线程的开销更大:工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他JS环境必需的特性。创建这些结构的代价不容忽视
2.Worker的类型Worker线程规范中定义了三种主要的工作者线程
{专用工作线程|DedicatedWebWorker}专用工作者线程,通常简称为工作者线程、WebWorker或Worker,是一种实用的工具,可以让脚本单独创建一个JS线程,以执行委托的任务。只能被创建它的页面使用
{共享工作线程|SharedWebWorker}:共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息
{服务工作线程|ServiceWorker}:主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色
3.WorkerGlobalScope在网页上,window对象可以向运行在其中的脚本暴露各种全局变量。
在Worker线程内部,没有window的概念
全局对象是WorkerGlobalScope的实例,通过self关键字暴露出来
WorkerGlobalScope属性
WorkerGlobalScope方法
self上可用的属性/方法是window对象上属性/方法的严格子集
2.{专用工作线程|DedicatedWebWorker}专用工作线程是最简单的Web工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。
其实,我们平时在工作中,遇到的最多的也是专用工作线程。
基本概念把专用工作线程称为{后台脚本|backgroundscript}
JS线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。
创建工作线程创建工作线程最常见的方式是加载JS文件:即把文件路径提供给Worker构造函数,然后构造函数再在后台异步加载脚本并实例化工作线程。
worker.js//?进行密集计算?bala?balamain.jsconst?worker?=?new?Worker(?'worker.js');console.log(worker);?//?Worker?{}?//?{3}这里有几个点需要注意下:
这个文件(worker.js)是在后台加载的,工作线程的初始化完全独立于main.js
工作线程本身存在于一个独立的JS环境中,因此main.js必须以Worker对象为代理实现与工作线程通信
在{3}行,虽然相应的工作线程可能还不存在,但该Worker对象已在原始环境中可用了
安全限制工作线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作线程的脚本文件会导致错误,如下所示:
假设父页面为https://bcnz.com
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');在创建remoteOriginWorker时,页面报错。
//?Error:?Uncaught?DOMException:?//?Failed?to?construct?'Worker'://?Script?at?https://wl.com/main.js?cannot?be?accessed//?from?origin?https://bcnz.com不能使用非同源脚本创建工作线程,并不影响执行其他源的脚本
使用Worker对象Worker()构造函数返回的Worker对象是与刚创建的专用工作线程通信的连接点
Worker对象可用于在工作线程和父上下文间传输信息,以及捕获专用工作线程发出的事件。
Worker对象支持下列事件处理程序属性:
onerror:在工作线程中发生ErrorEvent类型的错误事件时会调用指定给该属性的处理程序
该事件会在工作线程中抛出错误时发生
该事件也可以通过worker.addEventListener('error',handler)的形式处理
onmessage:在工作线程中发生MessageEvent类型的消息事件时会调用指定给该属性的处理程序
该事件会在工作线程向父上下文发送消息时发生
该事件也可以通过使用worker.addEventListener('message',handler)处理
onmessageerror:在工作线程中发生MessageEvent类型的错误事件时会调用指定给该属性的处理程序
该事件会在工作线程收到无法反序列化的消息时发生
该事件也可以通过使用worker.addEventListener('messageerror',handler)处理
Worker对象还支持下列方法
postMessage():用于通过异步消息事件向工作线程发送信息。
terminate():用于立即终止工作线程。没有为工作线程提供清理的机会,脚本会突然停止
DedicatedWorkerGlobalScope在专用工作线程内部,全局作用域是DedicatedWorkerGlobalScope的实例。
因为这继承自WorkerGlobalScope,所以包含它的所有属性和方法。工作线程可以通过self关键字访问该全局作用域。
globalScopeWorker.jsconsole.log('inside?worker:',?self);main.jsconst?worker?=?new?Worker('./globalScopeWorker.js');console.log('created?worker:',?worker);//?created?worker:?Worker?{}//?inside?worker:?DedicatedWorkerGlobalScope?{}两个独立的JS线程都在向一个console对象发消息,该对象随后将消息序列化并在浏览器控制台打印出来。浏览器从两个不同的JS线程收到消息,并按照自己认为合适的顺序输出这些消息。
DedicatedWorkerGlobalScope在WorkerGlobalScope基础上增加了以下属性和方法
name:可以提供给Worker构造函数的一个可选的字符串标识符。
postMessage():与worker.postMessage()对应的方法,用于从工作线程内部向父上下文发送消息
close():与worker.terminate()对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止
importScripts():用于向工作线程中导入任意数量的脚本
生命周期调用Worker()构造函数是一个专用工作线程生命的起点
调用之后,它会初始化对工作线程脚本的请求,并把Worker对象返回给父上下文。虽然父上下文中可以立即使用这个Worker对象,但与之关联的工作线程可能还没有创建,因为存在请求脚本的网格延迟和初始化延迟。
一般来说,专用工作线程可以非正式区分为处于下列三个状态:{初始化|initializing}、{激活|active}和{终止|terminated}。这几个状态对其他上下文是不可见的。虽然Worker对象可能会存在于父上下文中,但也无法通过它确定工作者线程当前是处理初始化、活动还是终止状态。
初始化时,虽然工作线程脚本尚未执行,但可以先把要发送给工作线程的消息加入队列。这些消息会等待工作线程的状态变为活动,再把消息添加到它的消息队列。
initializingWorker.jsself.addEventListener('message',?({data})?=>?console.log(data));main.jsconst?worker?=?new?Worker('./initializingWorker.js');//?Worker?可能仍处于初始化状态//?但?postMessage()数据可以正常处理worker.postMessage('foo');worker.postMessage('bar');worker.postMessage('baz');//?foo//?bar//?baz可以看到,在主线程中,创建了对应工作线程对应的Worker对象,在还未知道工作线程是否已经初始化完成,便可以直接通过postMessage进行线程之间通信。
创建之后,专用工作线程就会伴随页面的整个生命期而存在,除非自我终止(self.close())或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。
只要工作线程仍存在,与之关联的Worker对象就不会被当成垃圾收集掉
在整个生命周期中,一个专用工作线程只会关联一个网页(也称文档)。除非明确终止,否则只要关联文档存在,专用工作线程就会存在。
Worker选项Worker()构造函数允许将可选的配置对象作为第二个参数。
name:可以在工作线程中通过self.name读取到的字符串标识符。
type:表示加载脚本的运行方式,可以是classic或module。
classic将脚本作为常规脚本来执行
module将脚本作为模块来执行
credentials:在type为module时,指定如何获取与传输凭证数据相关的工作线程模块脚本。值可以是omit、same-orign或include。这些选项与fetch()的凭证选项相同。
行内创建工作线程基于Blob专用工作线程也可以基于Blob实例创建URL对象在行内脚本创建。
//?创建要执行的?JavaScript?代码字符串const?workerScript?=?`?self.onmessage?=?({data})?=>?console.log(data);`;//?基于脚本字符串生成?Blob?对象const?workerScriptBlob?=?new?Blob([workerScript]);//?基于?Blob?实例创建对象?URLconst?workerScriptBlobUrl?=?URL.createObjectURL(workerScriptBlob);//?基于对象?URL?创建专用工作者线程const?worker?=?new?Worker(workerScriptBlobUrl);worker.postMessage('blob?worker?script');//?blob?worker?script通过脚本字符串创建了Blob
然后又通过Blob创建了URL对象
最后把URL对象,传给了Worker()构造函数
基于函数序列化函数的toString()方法返回函数代码的字符串,而函数可以在父上下文中定义但在子上下文中执行
function?fibonacci(n)?{?return?n?<?1???0?:?n?<=?2???1?:?fibonacci(n?-?1)?+?fibonacci(n?-?2);}const?workerScript?=?`?self.postMessage(???(${fibonacci.toString()})(9)?);`;const?worker?=?new?Worker(URL.createObjectURL(new?Blob([workerScript])));worker.onmessage?=?({data})?=>?console.log(data);//?34像这样序列化函数有个前提,就是函数体内不能使用通过闭包获得的引用,也包括全局变量。
动态执行脚本工作线程可以使用importScripts()方法通过编程方式加载和执行任意脚本
这个方法会加载脚本并按照加载顺序同步执行。
//Worker.js
scriptA.jsconsole.log('scriptA?executes');scriptB.jsconsole.log('scriptB?executes');worker.jsconsole.log('importing?scripts');importScripts('./scriptA.js');importScripts('./scriptB.js');console.log('scripts?imported');Main.js
const?worker?=?new?Worker('./worker.js');//?importing?scripts//?scriptA?executes//?scriptB?executes//?scripts?importedimportScripts()方法可以接收任意数量的脚本作为参数。执行会严格按照它们在参数列表的顺序进行。
脚本加载受到常规CORS的限制,但在工作线程内部可以请求来自任何源的脚本
在这种情况下,所有导入的脚本也会共享作用域。
Worker.js
scriptA.jsconsole.log(`scriptA?executes?in?${self.name}?with?${globalToken}`);scriptB.jsconsole.log(`scriptB?executes?in?${self.name}?with?${globalToken}`);worker.jsconst?globalToken?=?'wl';console.log(`importing?scripts?in?${self.name}?with?${globalToken}`);importScripts('./scriptA.js',?'./scriptB.js');console.log('scripts?imported');main.js
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');0与专用工作线程通信与工作线程的通信都是通过异步消息完成的
使用postMessage()是使用postMessage()传递序列化的消息。
factorialWorker.js
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');1main.js
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');2对于传递简单的消息,使用postMessage()在主线程和工作者线程之间传递消息。并且没有targetOrigin的限制。
然后还可以使用MessageChannel/BroadcastChannel进行线程之间的通信,这里就不展开说明了。但是大部分,用postMessage()就够用了
数据传输工作线程是独立的上下文,因此在上下文之间传输数据就会产生消耗。
在JS中,有三种在上下文间转移信息的方式:
{结构化克隆算法|structuredclonealgorithm}、
{可转移对象|transferableobjects}
{共享数组缓冲区|?sharedarraybuffers}
结构化克隆算法结构化克隆算法可用于在两个独立上下文间共享数据
在通过postMessage()传递对象时,浏览器会遍历该对象,并在目标上下文中生成它的一个副本。
结构化克隆算法支持的类型

需要注意的点
结构化克隆算法在对象比较复杂时会存在计算性消耗。因此,实践中要尽可能避免过大、过多的复制。
可转移对象使用可转移对象可以把所有权从一个上下文转移到另一个上下文。在不太可能在上下文间复制大量数据的情况下,这个功能特别有用。
可转移对象支持的类型
ArrayBuffer
MessagePort
ImageBitmap
OffscreenCanvas
postMessage()方法的第二个可选参数是数组,它指定应该将哪些对象转移到目标上下文。在遍历消息负载对象时,浏览器根据转移对象数组检查对象引用,并对转移对象进行转移而不复制它们。
把ArrayBuffer指定为可转移对象,那么对缓冲区内存的引用就会从父上下文中抹去,然后分配给工作者线程。
main.js
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');3worker.js
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');4共享数组缓冲区既不克隆,也不转移,SharedArrayBuffer作为ArrayBuffer能够在不同浏览器上下文间共享。
在把SharedArrayBuffer传给postMessage()时,浏览器只会传递原始缓冲区的引用。结果是,两个不同的js上下文会分别维护对同一个内存块的引用。
专用工作线程+Webpack假设存在如下的一种文档结构
//?尝试基于?与父页面同源的脚本创建工作者线程const?sameOriginWorker?=?new?Worker('./worker.js');//?尝试基于?https://wl.com/worker.js?创建工作者线程?(与父页面不同源)const?remoteOriginWorker?=?new?Worker('https://wl.com/worker.js');5行内创建工作线程就像上面介绍的一样,我们可以借用行内方式来创建一个工作线程来,维护一些比较耗时的操作。
在longTime.js中