• Lv0
    粉丝0

积分14 / 贡献0

提问0答案被采纳0文章4

作者动态

[经验分享] Web Worker,JavaScript 多线程

liubo-688 显示全部楼层 发表于 2024-12-17 14:49:35

我们都知道,JavaScript是单线程的语言,这是因为最初 js 被设计用于浏览器中,主要用于操作dom元素,实现用户和浏览器的交互,如果 js 是多线程,那么可能会出现多个线程同时操作一个dom元素的情况,造成浏览器混乱,所以为了避免这种复杂性,js 被设计成相对简单的单线程的语言。

但是,随着技术的发展,在多核CPU的时代,单线程无法充分发挥计算机的计算能力,于是,在HTML5中,提出了 Web Worker 标准。Web Worker 可以为 JavaScript 脚本创建多个线程,让我们可以将一些复杂的计算任务交给worker线程运行,避免主线程阻塞,等到Worker线程计算完毕再把结果返回主线程。但是子线程完全受主线程控制,有多种限制条件,包括不能操作DOM等。所以,Web Worker并没有改变js单线程的本质。

线程的分类

Web Worker线程其实可以分为几类,例如

  • Dedicated Worker:专用线程
  • Shared Worker:共享线程
  • Service Workers:服务工作线程

......等等

本文主要讲的是Dedicated Worker,即专用线程,专用线程只能被创建它的脚本使用,一个专用线程对应着一个主线程,一般情况下,Web Worker线程运行的代码就是为了当前的脚本(页面)服务,所以专用线程也是最常用的一种线程。

初步使用

创建 Worker 线程

Worker 线程的创建通过Worker 构造函数

Worker 构造函数的第一个参数是一个脚本文件,这个文件不能是本地文件,因为Worker无法读取本地文件,直接写本地地址会报错,所以这个脚本文件必须来自服务器。

第二个可选参数是一个options对象,可以配置name值来指定Worker的名称,可用于区分多个Worker。

可以理解为当前创建worker线程的代码就是主线程,上面Worker 构造函数的参数脚本文件就是worker线程。

主线程与Worker线程通信

主线程收发数据

主线程创建 Worker 线程后,可以通过 <span lang="EN-US">worker.postMessage()</span> 方法向Worker发送数据。该数据可以是各种数据类型,包括二进制数据等。

然后通过 <span lang="EN-US">worker.onmessage</span> 或者 <span lang="EN-US">worker.addEventListener('message', function(){})</span>的方式接收Worker线程发送过来的数据。

Worker线程收发数据

同样,Worker线程可以通过 <span lang="EN-US">self.postMessage()</span>给主线程发送数据

通过 <span lang="EN-US">self.onmessage</span>或者 <span lang="EN-US">self.addEventListener('message', function(){})</span>的方式接收

值得一提的是在Worker线程中,self 代表线程自身(主线程中self代表window),也可以用this代替self,或者干脆省略不写也是可以的,所以下面三种写法其实是一样的

数据通信例子

例如我们有段worker线程的代码如下

同时主线程的代码如下

最终打印结果为

可以发现上面代码 Worker线程在接收主线程发过来的数据后将其对象数据上的某个值修改,然后发送数据给主线程,主线程在接收时打印原先发生的data数据,发现name值没变,说明主线程和worker线程的这种数据通信是拷贝关系,而不是简单的传值。所以Worker线程对通信数据的修改并不会影响主线程的数据。

作者:凯哥爱吃皮皮虾 链接:https://juejin.cn/post/7085011669583134734 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

worker线程的限制

· Worker 脚本文件的限制

  • Worker 线程无法读取本地文件,脚本文件需来自服务器
  • 同源策略:Worker 线程运行的脚本文件,必须与主线程的脚本文件同源

· Worker 线程全局对象限制

  • Dom限制:如前面所说,为了避免多个线程同时操作dom带来的复杂性,Worker线程不能访问 <span lang="EN-US">document</span><span lang="EN-US">window</span><span lang="EN-US">parent</span>对象。但是可以访问 <span lang="EN-US">navigator</span>对象和 <span lang="EN-US">location</span>对象
  • Worker线程也无法使用 <span lang="EN-US">alert()</span>方法和 <span lang="EN-US">confirm()</span>方法。但是Worker可以访问XMLHttpRequest 对象,也就是发AJAX请求,也可以获取setTimeout(), clearTimeout(), setInterval(), clearInterval()等定时操作方法

· 数据通信限制:如上面所说,Worker线程和主线程并不在同一个上下文环境,不能直接通信。

本地调试方案

脚本文件必须来自网络,那么我们在本地调试的时候怎么调试呢?

其实方案有很多,这里给出几个常用的比较简单的方案供大家参考。

利用Blob

我们可以通过Blob()方式,首先

我们可以把Worker线程代码写出字符串的形式,再通过 new Blob() 和 window.URL.createObjectURL() 的方式来将其转化为可以生效的 worker 脚本文件

如果不想通过这种字符串的形式,也可以用一个 script 标签将Worker线程代码包裹起来,并将其type类型设置为js无法识别的自定义类型,那么它就会被认为是一个数据块元素。

示例代码:

然后通过document的方式获取其代码

使用http-server

通过安装http-server

然后在相关代码的文件夹下运行命令

这样,便可以创建worker

Worker的错误处理

在主线程中通过 <span lang="EN-US">worker.onerror</span> 或者 <span lang="EN-US">worker.addEventListener('onerror', function(){})</span>的方式来监听Worker是否发生错误。

例如,

Worker线程代码:

主线程代码:

上面代码执行后worker线程中由于 undefined 上面没有 foreach 方法,于是报错如下

Worker线程中也可以监听错误,不过只能拿到错误的消息数据

关闭Worker

Worker一旦创建成功就会始终运行,所以Worker 也比较耗费资源,当Worker 使用完毕时,我们可以手动停止Worker,通过以下代码

也可以在Worker线程中关闭

Worker中加载其他脚本

Worker内部如果需要加载其他的脚本,可以通过 <span lang="EN-US">importScripts()</span>来加载

举个简单的例子

下面代码是被加载的文件

Worker线程的代码如下

在主线程运行创建出上面的Worker线程后,控制台可以如期打印出 3 (1+2=3),也就是Worker线程成功引入了 <span lang="EN-US">otherScript.js </span>文件的代码。

同时引入多个文件

importScripts() 也可以同时引入多个文件

importScripts的阻塞性

importScripts 是同步的执行代码的,并且有一定的阻塞性,我们看下面两个实验。

首先,有以下的test.js文件代码

Worker 线程代码

主线程创建上面Worker 的线程运行后可以在控制台发现

时间大概是 8 毫秒。

而相对的,我们的我们直接把那段代码写到Worker线程中

Worker 线程代码

打印结果为

时间大概是 3 毫秒。

所以,其实importScripts并不是很实用。

在实际开发中,我们肯定是用模块化开发,不可能把Worker的所有代码都写在一个文件上,那么在 importScripts 不实用的情况下,我们可以使用打包工具将Worker的代码打包成一个文件,例如在webpack的项目中,我们可以使用 webworkify-webpack 插件。

webworkify-webpack

笔者之前做过一个在线教育的直播平台,在对师生聊天历史记录的数据处理时,就用到了webworkify-webpack这个插件创建Worker线程。

安装webworkify-webpack

在webpack项目下安装

webworkify-webpack的使用

首先,Worker线程代码需要在包裹在函数中,并用module.exports导出,该函数的参数就是该Worker线程的self。如下示例

而主线程中,创建对应Worker如下示例

上面代码中,work的参数可以用 <span lang="EN-US">require.resolve</span>来返回Worker线程文件的绝对路径,require.resolve还可以检查拼接好之后的路径是否存在。

webworkify-webpack原理

以下是webworkify-webpack源码的部分截取,可以发现其原理和上面的通过Blob()方式创建Worker线程一样都是通过 Blob 的方式

webworkify-webpack实例

接下来,我们结合Vue写一个计时器示例:

首先,创建Worker线程文件

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部