ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

WebWorker:工作者线程初探

2022-05-06 21:00:28  阅读:164  来源: 互联网

标签:postMessage Worker self worker WebWorker 线程 初探 工作者


WebWorker:工作者线程初探

参考资料:

1.Web Worker 使用教程 - 阮一峰:http://www.ruanyifeng.com/blog/2018/07/web-worker.html

2.JavaScript高级程序设计-第四版

一、概述

​ JavaScript 是单线程的,单线程就意味着不能像多线程语言那样把工作委托给独立的线程或进程去做,无法充分发挥现代计算机多核CPU的优势。

​ Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

​ Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

web worker的一些特点:

  • 同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。也就是说只能加载来自网络的脚本文件,无法读取本地文件
  • DOM 限制:Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象,自然也不能alert(),但是,Worker 线程可以navigator对象和location对象。
  • 实际线程:工作者线程是以实际线程实现的。但不一定在同一个进程里(一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于)
  • 并行执行:虽然页面和工作者线程都是单线程 JavaScript 环境,每个环境中的指令则可以并行执行
  • 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成

二、工作者线程的类型

Web 工作者线程规范中定义了三种主要的工作者线程:专用工作者线程、共享工作者线程和服务工作者线程:

  • 专用工作者线程:通常简称为工作者线程、Web Worker 或 Worker,,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务,只能被创建它的页面使用
  • 共享工作者线程:共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息
  • 服务工作者线程:主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色

全局对象WorkerGlobalScope:

​ 在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有 window的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来。

// 在正常环境中
console.log('self',self); // -> 打印结果是Window对象
// 在worker环境中
const worker_code = () => {

    // 向主线程发送一条消息
    self.postMessage('worker loaded successfully...');

    // 监听主线程发来的信息
    self.onmessage = (e) => {
        console.log('接收到的event:',e);
        let result = `从主线程接收到的数据 ${e.data}`;
        self.postMessage(result)
    }

    console.log('self',self);
}

从结果上来看,self 上可用的属性是 window 对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本,不同的环境中,self指向结果是不一样的,

在主线程环境中,self指向window对象。在工作者线程中,self指向具体的WorkerGlobalScope的实例,比如在专用工作者线程中,self的指向的具体实例对象是

DedicatedWorkerGlobalScope。

WorkerGlobalScope 的子类:

​ 实际上并不是所有地方都实现了 WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这继承自 WorkerGlobalScope

  • 专用工作者线程使用 DedicatedWorkerGlobalScope
  • 共享工作者线程使用 SharedWorkerGlobalScope
  • 服务工作者线程使用 ServiceWorkerGlobalScope

三、用法示例

​ 这里以专用工作者线程为例,看看他的用法:

创建一个worker-test.js,内容如下

const worker_code = () => {

    // 向主线程发送一条消息
    self.postMessage('worker loaded successfully...');

    // 监听主线程发来的信息
    self.onmessage = (e) => {
        console.log('接收到的event:',e);
        let result = `从主线程接收到的数据 ${e.data}`;
        self.postMessage(result)
    }

    console.log('self',self);
}

let code = worker_code.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
console.log(code);

const blob = new Blob([code],{ type: 'application/javascript' });
const worker_script = URL.createObjectURL(blob);

export default  worker_script;

在主环境中创建Worker:

const worker = new Worker(worker_script, { name: 'myWorker' });
console.log(worker);
worker.onmessage = (event) => {
    console.log('主线程接收到了消息:',event.data)
}

// 点击按钮向工作者线程发送消息
const handleClick = () => {
    worker.postMessage('这是一条主线程发送的消息')
}

<button onclick="handleClick()">发送消息</button>

点击按钮并发送消息,完成主线程与工作者线程的通信,这样就完成了一个简单的通信例子。

worker构造函数接收两个参数,第一个参数是要加载的脚本内容,这个脚本既可以从url获取,也可以传入一个字符串格式的javascript代码,第二个参数则是配置信息,比如可以指定创建worker的名称。

使用模板字符串形式

​ 上面的例子通过把function转换成string再由Blob创建工作者线程,另一种写法是,通过模板字符串直接写js代码

// 创建要执行的 JavaScript 代码字符串
const workerScript = ` 
 self.onmessage = ({data}) => console.log(data); 
`;
// 基于脚本字符串生成 Blob 对象
const workerScriptBlob = new Blob([workerScript]); 
// 基于 Blob 实例创建对象 URL 
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob); 
// 基于对象 URL 创建专用工作者线程
const worker = new Worker(workerScriptBlobUrl); 
worker.postMessage('blob worker script');

四、详细介绍

​ 上面内容初步介绍了web worker的一些概念以及简单用法,下面将详细介绍工作者线程的内容(这里以专用工作者线程为例)

​ 专用工作者线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务。

加载worker的限制:

工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误

const sameOriginWorker = new Worker('./worker.js'); // 尝试基于当前路径源加载,可行

const remoteOriginWorker = new Worker('https://untrusted.com/worker.js'); // 跨域加载,将会报错

work对象:

​ 构建出来的worker对象拥有如下API:

方法 说明
onerror 监听在在工作者线程中发生 ErrorEvent 类型的错误事件,也可以通过 worker.addEventListener('error', handler)的形式处理
onmessage 监听在工作者线程中发生 MessageEvent 类型的消息事件,例如工作者线程中使用postMessage发送消息
onmessageerror 监听在工作者线程中发生 MessageEvent 类型的错误事件
postMessage 发送消息(异步)
terminate 用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止

DedicatedWorkerGlobalScope实例:

​ 在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例,继承WorkerGlobalScope,可以通过 self 关键字访问该全局作用域

DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法:

方法 说明
name 给worker起个名字
postMessage 与之关联的上下文发送消息
close 与worker.terminate()方法一样,立即终止工作者线程(先取消事件循环里的任务)
importScripts 用于向工作者线程中导入脚本,可以跨域

生命周期:

​ 从const worker = new Worker()开始,worker的生命周期就存在了,一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始(initializing)、活动(active) 和终止(terminated)。为什么是非正式的状态,因为其实这几个状态对其他上下文是不可见的,比如主线程环境中其实无法知道web worker的加载状态,也没有相关api来表明能够监听到具体的生命周期执行阶段。

const worker = new Worker('./initializingWorker.js'); 
// Worker 可能仍处于初始化状态
// 但 postMessage()数据可以正常处理
worker.postMessage('foo'); 
worker.postMessage('bar'); 
worker.postMessage('baz');

​ 初始化时,虽然工作者线程脚本尚未执行(可能由于网络原因,initializingWorker还未传输完毕),但可以先把要发送给工作者线程的消息加入队列。这些消息会等待工作者线程的状态变为活动,再把消息添加到它的消息队列。

​ 创建之后,专用工作者线程就会伴随页面的整个生命期而存在,除非自我终止(self.close())或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。只要工作者线程仍存在,与之关联的 Worker 对象就不会被当成垃圾收集掉。

终止生命周期:self.close()方法和worker.terminate()方法都会终结掉工作者线程,但是他们的行为有些差异

自我终止:

// closeWorker.js
self.postMessage('foo'); 
self.close(); 
self.postMessage('bar'); 
setTimeout(() => self.postMessage('baz'), 0);

// main.js
const worker = new Worker('./closeWorker.js'); 
worker.onmessage = ({data}) => console.log(data);

​ 自我终止不会立即终止,close()在这里会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务,工作者线程不需要执行同步停止,因此在父上下文的事件循环中处理的"bar"仍会打印出来。

外部终止:

// terminateWorker.js
self.onmessage = ({data}) => console.log(data); 

// main.js
const worker = new Worker('./terminateWorker.js'); 
// 给 1000 毫秒让工作者线程初始化
setTimeout(() => { 
 worker.postMessage('foo'); 
 worker.terminate(); 
 worker.postMessage('bar'); 
 setTimeout(() => worker.postMessage('baz'), 0); 
}, 1000); 
// foo

这里,外部先给工作者线程发送了带"foo"的 postMessage,这条消息可以在外部终止之前处理。一旦调用了 terminate(),工作者线程的消息队列就会被清理并锁住,这也是只是打印"foo"的原因。

​ 在整个生命周期中,一个专用工作者线程只会关联一个网页(Web 工作者线程规范称其为一个文档)。除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。

配置项:

new worker的第二个参数有如下配置项:

配置项 说明
name 工作者线程的名字
type 表示加载脚本的运行方式,可以是"classic"或"module"。"classic"将脚本作为常规脚本来执行,"module"将脚本作为模块来执行。
credentials 传输凭证:值可以是"omit"、"same-orign"或"include"

使用importScripts导入其他脚本:

​ 在工作者线程内部,可以使用importScripts导入任意数量的脚本,并且可以跨域

importScripts('./scriptA.js'); 
importScripts('./scriptB.js'); // 等同于 importScripts('./scriptA.js', './scriptB.js');

五、在React中使用web worker

​ 很幸运,react-webworker-hook提供了hooks钩子函数来简化webworker的使用方式,代码示例如下:

首先安装工具包

npm install --save react-webworker-hook

使用它计算斐波那契数值:

import React, { useState } from "react";
import useWebWorker from "react-webworker-hook";
 
function GenerateFibonacci() {
  const [data = 0, postData] = useWebWorker({
    url: "./webworker_example.js"
  });
  const [count, setCount] = useState(0);
 
  return (
    <div>
      {`fibonacci for ${count}: ${data}`}
      <button
        onClick={() => {
          setCount(count + 1);
          postData(count);
        }}
      >
        Generate
      </button>
    </div>
  );
}
 
export default GenerateFibonacci;

也可以通过useWebWorkerFromScript钩子函数直接书写字符串形式

import React, { useState }                      from "react";
import { useWebWorkerFromScript } from "react-webworker-hook";

function GenerateFibonacci() {

    const [data = 0, postData] = useWebWorkerFromScript(`
        const fib = n => (n < 2 ? n : fib(n - 1) + fib(n - 2));
        onmessage = ({ data }) => {
          postMessage(fib(data));
        };
    `);

    const [count, setCount] = useState(0);

    return (
        <div>
            {`fibonacci for ${count}: ${data}`}
            <button
                onClick={() => {
                    setCount(count + 1);
                    postData(count);
                }}
            >
                计算
            </button>
        </div>
    );
}

export default GenerateFibonacci;

六、不同类型的工作者线程对比

类型 通信方式 使用场景
Worker postMessage 适合大量计算的场景,例如斐波那契计算
SharedWorker port.postMessage 适合跨 tab、iframes之间共享数据
ServiceWorker 单向通信,通过
addEventListener 监听
serviceWorker 的状态
缓存资源、网络优化

以上就是Web Worker初探内容,实际上还有共享工作者线程和服务工作者线程还未说到。

标签:postMessage,Worker,self,worker,WebWorker,线程,初探,工作者
来源: https://www.cnblogs.com/suanyunyan/p/16230405.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有