Web Worker入门(内部分享笔记)

1,014 阅读8分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

本文是摘取笔者在一次组内分享的笔记。因为时间限制,主要为了向组员介绍Web Worker基本知识,本篇文章现在来看,还是讲得不够深入。关于Web Worker,笔者在企业项目中确实没有用到,如果你们真在实际项目中用到,欢迎交流,带我进步。

前置知识

为了让同学们更好理解Web Worker,简单地带大家走一片相关的前置知识。

进程和线程

类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“ 这样的回答感觉太抽象,都不太容易让人理解。

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
  • 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
  • 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
  • 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁"
  • 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

浏览器是多进程的

对浏览器进行一定程度上的认识:

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程

不信可以自己打开浏览器多个tab页面,windows对应可以查看自己的任务管理器,进程管理中多少个tab页面,系统进程中就显示多少个浏览器进程。(其实严格上讲,每一个Tab标签对应一个进程并不一定是绝对的。)

image.png image.png

相比于单进程浏览器,多进程有如下优点:

  • 避免单个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 的两个限制:

  1. 分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源
  2. 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);
    }    

image.png

Web Worker 主线程的其他 API:

1. 主线程与 worker 线程通信:

worker.postMessage({
  hello: ['hello', 'world']
});

它们相互之间的通信可以传递对象和数组,这样我们就可以根据相互之间传递的信息来进行一些操作,比如可以设置一个type属性,当值为hello时执行什么函数,当值为world的时候执行什么函数。

值得注意的是:它们之间通信是通过拷贝的形式来传递数据的,进行传递的对象需要经过序列化,接下来在另一端还需要反序列化。这就意味着:

  1. 我们不能传递不能被序列化的数据,比如函数,会抛出错误的。
  2. 在一端改变数据,另外一端不会受影响,因为数据不存在引用,是拷贝过来的。

监听 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');
复制代码
  1. 脚本中的全局变量都能被 worker 线程使用。
  2. 脚本的下载顺序是不固定的,但执行时会按照传入 importScripts() 中的文件名顺序进行,这个过程是同步的。

Worker 线程限制

因为 worker 创造了另外一个线程,不在主线程上,相应的会有一些限制,我们无法使用下列对象

  1. window 对象
  2. document 对象
  3. DOM 对象
  4. parent 对象

我们可以使用下列对象/功能

  1. 浏览器:navigator 对象
  2. URL:location 对象,只读
  3. 发送请求:XMLHttpRequest 对象
  4. 定时器:setTimeout/setInterval,在 worker 线程轮询也是很棒!
  5. 应用缓存:Application Cache

应用场景:

  1. 数学运算

  2. 图像、影音等文件处理

  3. 大量数据检索

    比如用户输入时,我们在后台检索答案,或者帮助用户联想,纠错等操作.

  4. 耗时任务都丢到 webworker 解放我们的主线程。

如一个面试题:当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

学了Web Worker,就可以基于Web Worker去解决这种耗时任务。

参考文章

线程和进程的区别是什么? - 知乎 (zhihu.com)

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理 (juejin.cn)

Web Worker 使用教程 - 阮一峰的网络日志 (ruanyifeng.com)

前端er来学习一下webWorker吧 (juejin.cn)