本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
前言
本文是摘取笔者在一次组内分享的笔记。因为时间限制,主要为了向组员介绍Web Worker
基本知识,本篇文章现在来看,还是讲得不够深入。关于Web Worker
,笔者在企业项目中确实没有用到,如果你们真在实际项目中用到,欢迎交流,带我进步。
前置知识
为了让同学们更好理解Web Worker
,简单地带大家走一片相关的前置知识。
进程和线程
类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“ 这样的回答感觉太抽象,都不太容易让人理解。
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
浏览器是多进程的
对浏览器进行一定程度上的认识:
- 浏览器是多进程的
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
- 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程
不信可以自己打开浏览器多个tab页面,windows对应可以查看自己的任务管理器,进程管理中多少个tab页面,系统进程中就显示多少个浏览器进程。(其实严格上讲,每一个Tab标签对应一个进程并不一定是绝对的。)
相比于单进程浏览器,多进程有如下优点:
- 避免单个page crash影响整个浏览器
- 避免第三方插件crash影响整个浏览器
- 多进程充分利用多核优势
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果是单进程,插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。
JS是单线程的
JavaScript
语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为什么javascript不能有多个线程呢?
JavaScript
的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker
标准,允许 JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。
Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。所以,这个新标准并没有改变 JavaScript 单线程的本质。
Web Worker
Web Worker
的作用,就是为JavaScript
创造多线程环境,允许主线程创建Worker
线程,将一些任务分配给后者运行。在主线程运行的同时,Worker
线程在后台运行,两者互不干扰。等到Worker
线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被Worker
线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker
线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了Worker
比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。 -- 阮一峰《Web Worker 使用教程》
具体的使用语法可以参考阮一峰这篇文章:Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)
因为Web Worker
的两个限制:
- 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
- worker 不能读取本地的文件(不能打开本机的文件系统
file://
),它所加载的脚本必须来自网络。
演示中采用另外一种方式:
const data = `
// worker线程加载脚本 TODO: Worker 线程无法读取本地文件,加载的脚本必须来自网络
console.log('worker线程开始执行')
// 监听主线程传过来的信息
self.onmessage = e => {
console.log('主线程传来的信息:', e.data);
};
// 发送信息给主线程
self.postMessage('来自worker线程');
// 关闭worker线程
function closeSon() {
return self.close();
}`;
// 以字符串形式创建worker线程,把代码字符串,转成二进制对象,生成 URL,加载URL
const blob = new Blob([data]);
const url = window.URL.createObjectURL(blob);
console.log(url)
var worker = new Worker(url)
worker.postMessage('Hello World');
worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}
Web Worker
主线程的其他 API:
1. 主线程与 worker 线程通信:
worker.postMessage({
hello: ['hello', 'world']
});
它们相互之间的通信可以传递对象和数组,这样我们就可以根据相互之间传递的信息来进行一些操作,比如可以设置一个type
属性,当值为hello
时执行什么函数,当值为world
的时候执行什么函数。
值得注意的是:它们之间通信是通过拷贝的形式来传递数据的,进行传递的对象需要经过序列化,接下来在另一端还需要反序列化。这就意味着:
- 我们不能传递不能被序列化的数据,比如函数,会抛出错误的。
- 在一端改变数据,另外一端不会受影响,因为数据不存在引用,是拷贝过来的。
监听 worker 线程返回的信息
worker.onmessage = function (e) {
console.log('父进程接收的数据:', e.data);
// doSomething();
}
主线程关闭 worker 线程
Worker 线程一旦新建成功,就会始终运行,这样有利于随时响应主线程的通信。
这也是 Worker 比较耗费计算机的计算资源(CPU
)的原因,一旦使用完毕,就应该关闭 worker 线程。
worker.terminate(); // 主线程关闭worker线程
4. 监听错误
// worker线程报错
worker.onerror = e => {
// e.filename - 发生错误的脚本文件名;e.lineno - 出现错误的行号;以及 e.message - 可读性良好的错误消息
console.log('onerror', e);
};
Worker 线程
self 代表 worker 进程自身
worker 线程的执行上下文是一个叫做WorkerGlobalScope
的东西跟主线程的上下文(window)不一样。
我们可以使用self
/WorkerGlobalScope
来访问全局对象。
监听主线程传过来的信息:
self.onmessage = e => {
console.log('主线程传来的信息:', e.data);
// do something
};
复制代码
发送信息给主线程
self.postMessage({
hello: [ '这条信息', '来自worker线程' ]
});
复制代码
worker 线程关闭自身
self.close()
复制代码
worker 线程加载脚本:
Worker 线程能够访问一个全局函数 imprtScripts()来引入脚本,该函数接受 0 个或者多个 URI 作为参数。
importScripts('http~.js','http~2.js');
复制代码
- 脚本中的全局变量都能被 worker 线程使用。
- 脚本的下载顺序是不固定的,但执行时会按照传入 importScripts() 中的文件名顺序进行,这个过程是同步的。
Worker 线程限制
因为 worker 创造了另外一个线程,不在主线程上,相应的会有一些限制,我们无法使用下列对象:
- window 对象
- document 对象
- DOM 对象
- parent 对象
我们可以使用下列对象/功能:
- 浏览器:navigator 对象
- URL:location 对象,只读
- 发送请求:XMLHttpRequest 对象
- 定时器:setTimeout/setInterval,在 worker 线程轮询也是很棒!
- 应用缓存:Application Cache
应用场景:
-
数学运算
-
图像、影音等文件处理
-
大量数据检索
比如用户输入时,我们在后台检索答案,或者帮助用户联想,纠错等操作.
-
耗时任务都丢到 webworker 解放我们的主线程。
如一个面试题:当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?
学了Web Worker
,就可以基于Web Worker
去解决这种耗时任务。
参考文章
从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 (juejin.cn)