Web Worker 是什么

# Web Worker 是什么

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。so bad,单线程现在基本是浪费CPU性能的

HTML5 提供并规范了 Web Worker 这样一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程(Worker 线程)中

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行

这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程就会很流畅,不会被阻塞或拖慢。

# Web Worker 的分类

Web Worker 根据工作环境的不同,可分为专用线程 Dedicated Worker共享线程 Shared Worker

  • Dedicated Worker的Worker只能从创建该Woker的脚本中访问

  • SharedWorker则可以被多个脚本所访问

在开发中如果使用到 Web Worker,目前大部分主要还是使用 Dedicated Worker的场景多,它只能为一个页面所使用,本文讲的也是这一类;而Shared Worker可以被多个页面共享,为跨浏览器 tab 共享数据提供了一种解决方案。

# Web Worker的使用限制

# 同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

# 文件限制

Worker 线程无法读取本地文件(file://),会拒绝使用 file 协议来创建 Worker实例,它所加载的脚本,必须来自网络

# DOM操作限制

Worker 线程所在的全局对象,与主线程不一样,区别是:

  • 无法读取主线程所在网页的 DOM 对象
  • 无法使用documentwindowparent这些对象

# 通信限制

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成,交互方法是postMessageonMessage,并且在数据传递的时候, Worker 是使用拷贝的方式。

# 脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求,也可以使用setTimeout/setInterval等API

# 基本 API

主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。

const worker = new Worker(aURL, options);
  • worker.postMessage: 向 worker 的内部作用域发送一个消息,消息可由任何 JavaScript 对象组成

  • worker.terminate: 立即终止 worker。该方法并不会等待 worker 去完成它剩余的操作;worker 将会被立刻停止

    使用完毕,为了节省系统资源,必须关闭 Worker。

    // 主线程
    worker.terminate();
    
    // Worker 线程
    self.close();
    
  • worker.onmessage:当 worker 的父级接收到来自其 worker 的消息时,会在 Worker 对象上触发 message 事件

  • worker.onerror: 当 worker 出现运行中错误时,它的 onerror 事件处理函数会被调用。它会收到一个扩展了 ErrorEvent 接口的名为 error 的事件

    worker.addEventListener('error', function (e) {
        console.log(e.message) // 可读性良好的错误消息
        console.log(e.filename) // 发生错误的脚本文件名
        console.log(e.lineno) // 发生错误时所在脚本文件的行号
    })
    

# 常见的使用方式

# 1. 直接指定脚本文件

const myWorker = new Worker(aURL, option)

aURL表示 worker 将执行的脚本的 URL(脚本文件), 即 Web Worker 所要执行的任务。

案例如下

主线程

// 主线程下创建worker线程
const worker = new Worker('./worker.js')

// 监听接收worker线程发的信息
worker.onmessage = function(e){
     console.log('主线程收到worker线程消息:', e.data)
}

// 向worker线程发送消息
worker.postMessage('主线程发送hello world')

worker.js

Worker 线程内部需要有一个监听函数,监听message事件。

// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {
  // e.data表示主线程发送过来的数据
  self.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});

Web Worker 的执行上下文名称是 self,无法调用主线程的 window 对象的。上述写法等同于以下写法:

this.addEventListener("message", function (e) {
  // e.data表示主线程发送过来的数据
  this.postMessage("worker线程收到的:" + e.data); // 向主线程发送消息
});

将JS文件引入html挂在本地开发环境运行,运行结果如下:

主线程收到worker线程消息: worker线程收到的:主线程发送hello world 

# 2. 使用 Blob URL 创建

除了这种通过引入js文件的方式,也可以通过URL.createObjectURL()创建URL对象,创建内嵌的worker

/**
 * const blob = new Blob(array, options);
 * Blob() 构造函数返回一个新的 Blob 对象。blob 的内容由参数数组中给出的值的串联组成。
 * @params array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array
 * @options type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。还有两个这里忽略不列举了
 */
 
/**
 * URL.createObjectURL():静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象
 */
const worker = new Worker(URL.createObjectURL(blob));

  • Blob 对象表示一个不可变、原始数据的类文件对象,它的数据可以按文本或二进制的格式进行读取。File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。
function func() {
  console.log('hello')
}

function createWorker(fn) {
  // const blob = new Blob([fn.toString() + ' fn()'], { type: 'text/javascript' })
  const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
  return URL.createObjectURL(blob)
}

createWorker(func)

载入与主线程在同一个网页的JavaScript 脚本代码

嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,比如下面的app/worker

<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

然后,读取这一段嵌入页面的脚本,用 Worker 来处理。

var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
 
worker.onmessage = function (e) {
// e.data === 'some message'
};

# Worker 线程中引入脚本

Worker线程内部要加载其他脚本,可以使用 importScripts()

// worker.js
importScripts("constants.js");

// self 代表子线程自身,即子线程的全局对象
self.addEventListener("message", function (e) {
  self.postMessage(foo); // 可拿到 `foo`、`getAge()`、`getName`的结果值 
});


// constants.js
const foo = "变量";

function getAge() {
  return 25;
}

const getName = () => {
  return "jacky";
};

还可以同时加载多个脚本

importScripts('script1.js', 'script2.js');

# 战应用场景

# 处理大量CPU耗时计算操作

大家最关心的还是 Web Worker 实战场景,开头我们说到,当有大量复杂计算场景时,可使用 Web Worker

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>worker计算</title>
  </head>
  <body>
    <div>计算从 1 到给定数值的总和</div>
    <input type="text" placeholder="请输入数字" id="num" />
    <button onclick="calc()">开始计算</button>
    <span>计算结果为:<span id="result">-</span></span>

    <div>在计算期间你可以填XX表单</div>
    <input type="text" placeholder="请输入姓名" />
    <input type="text" placeholder="请输入年龄" />

    <script>
      function calc() {
        const num = parseInt(document.getElementById('num').value)
        let result = 0
        let startTime = performance.now()
        // 计算求和(模拟复杂计算)
        for (let i = 0; i <= num; i++) {
          result += i
        }
        // 由于是同步计算,在没计算完成之前下面的代码都无法执行
        const time = performance.now() - startTime
        console.log('总计算花费时间:', time)
        document.getElementById('result').innerHTML = result
      }
    </script>
  </body>
</html>

image.png

如上,第一个输入框与按钮是负责模拟复杂计算的,比如输入 10000000000,点击开始计算,这时主线程处理一直在处理同步计算逻辑,在完成计算之前,会发现页面处于卡顿的状态,下方的两个输入框也无法点击交互,在我的电脑这部分计算是花了14s左右,这个卡顿时间给用户的体验就很差了。

打开控制台调用也可以看到这里CPU使用率是100%

image.png

如果把这部分计算交给 Web Worker 来处理,修改代码:

<script>
const worker = new Worker('./worker.js')

function calc() {
    const num = parseInt(document.getElementById('num').value)
    worker.postMessage(num)
}

worker.onmessage = function (e) {
    document.getElementById('result').innerHTML = e.data
}
</script>

./worker.js

function calc(num) {
  let result = 0
  let startTime = performance.now()
  // 计算求和(模拟复杂计算)
  for (let i = 0; i <= num; i++) {
    result += i
  }
  // 由于是同步计算,在没计算完成之前下面的代码都无法执行
  const time = performance.now() - startTime
  console.log('总计算花费时间:', time)
  self.postMessage(result)
}

self.onmessage = function (e) {
  calc(e.data)
}

然后重复上述一样的操作,输入 10000000000 计算,会发现下方两个输入框可正常流畅输入,整个页面也不卡顿。

Worker 运行独立于主线程的后台线程中,分担执行了大量占用CPU密集型的操作(但运行时间并不会变短),解放了主线程,主线程就能及时响应用户操作而不会造成卡顿的现象。使用Web Worker后,控制台工具可看到CPU使用率处于较低正常水平,计算过程跟没计算之前的水平一样。

image.png