1. 什么是WebWorker

众所周知,JavaScript 是单线程的言语。一切代码都运行在一个主线程中,包含处理用户界面,js代码履行和网络恳求。当履行耗时操作时,就会导致用户页面的卡顿和不响应,甚至浏览器直接卡死。现在前端遇到许多核算的场景越来越多,为了有更好的体会,HTML5 中提出了 Web Worker 的概念。

知识点回顾:为什么javaScript是单线程言语,多个线程不是更能进步效率吗?

JavaScript的单线程,与它的用处有关。作为浏览器脚本言语,JavaScript的主要用处是与用户互动,以及操作DOM。这决议了它只能是单线程,否则会带来很杂乱的同步问题。比方,假定JavaScript同时有两个线程,一个线程在某个DOM节点上增加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

Web Worker 是一种在浏览器中运行的 JavaScript 脚本,能够在独立的线程中履行,与主线程并行工作,供给了一种在后台履行杂乱核算或处理耗时操作的办法,而不会堵塞主线程的履行。它还能够与主线程进行通讯,经过信息传递机制来交换数据和成果,从而形成了高效、良好的用户体会。

Web Worker 是一个统称,详细能够细分为一般的 Worker、SharedWorker 和 ServiceWorker 等。

3种worker分别适合不同的场景,一般的 Worker 能够在需求许多核算的时分运用,创立新的线程能够降低主线程的核算压力,不会导致 UI 卡顿。SharedWorker 主要是为不同的 window、iframes 之间同享数据供给了别的一个解决计划。ServiceWorker 能够缓存资源,供给离线服务或许是网络优化,加速 Web 运用的敞开速度。

下面用一个表格大概了解一下三种worker的不同,本章要点介绍Worker相关内容

类型 Worker SharedWorker ServiceWorker
通讯办法 postMessage port.postMessage 单向通讯,经过 addEventListener 监听 serviceWorker 的状态
运用场景 适合许多核算的场景 适合跨 tab、iframes 之间同享数据 缓存资源、网络优化
兼容性 >= IE 10 >= Chrome 4 不支撑 IE、Safari、Android、iOS >= Chrome 4 不支撑 IE >= Chrome 40

提高用户体会计划之Web Worker—Worker1
web workers 现已被大多数浏览器支撑,运用上根本不用考虑兼容问题。

注意worker的实际上是一个较重的API,它对浏览器是有必定的担负的,所以主张需求细心剖析当时的场景是否是需求运用worker进行事务的完结或许性能的优化。

2. worker

worker运用Worker(...)结构来生成一个worker实例目标,他的界说主要如下

[Exposed=(Window,DedicatedWorker,SharedWorker)]
interface Worker : EventTarget {
  constructor(USVString scriptURL, optional WorkerOptions options = {});
  undefined terminate();
  undefined postMessage(any message, sequence<object> transfer);
  undefined postMessage(any message, optional StructuredSerializeOptions options = {});
  attribute EventHandler onmessage;
  attribute EventHandler onmessageerror;
};
dictionary WorkerOptions {
  WorkerType type = "classic";
  RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module"
  DOMString name = "";
};
enum RequestCredentials { "omit", "same-origin", "include" };
enum WorkerType { "classic", "module" };
Worker includes AbstractWorker;

2.1 结构函数

咱们能够看到worker承继于EventTarget,它本身就是选用的浏览器事情接口的。除此之外,结构函数接受两个参数分别是scriptURL以及options

  • scriptURL 用于指定要加载的worker模块的脚本地址,部分浏览器环境支撑运用data URI
  • options 目标类型,然后有三个可选的特点值,分别是type,credentials以及name
  • options.type 选择加载类型,能够运用的值有classic以及module,运用module答应运用ES Module对文件进行处理,支撑模块导入和导出,供给了更好的模块化支撑
  • options.credentials 用于拟定加载时文件时,是否带着cookie等身份认证信息,点击查看详细介绍
  • options.name worker的称号,据MDN上说一般用于调试

2.2 Worker的大局目标

首先咱们需求知道一下DedicatedWorkerGlobalScope的承继关系。

提高用户体会计划之Web Worker—Worker1

首先DedicatedWorkerGlobalScope承继于EventTargetWorkerGlobalScope。能够运用父类的办法。实际上worker也就是一个事情目标目标(EventTarget),它也能够挂载EventListener进行事情监听。那么在了解完承继关系后咱们再来整理一下WorkerGlobalScope供给了什么办法以及特点

EventTarget 是事情目标接口,用于处理事情。WorkerGlobalScope 承继了这个接口,使得 Worker 线程能够处理事情,例如 onmessageonerror

WorkerGlobalScope

WorkerGlobalScope 是 Web Workers 中的大局目标,相似于浏览器中的 window 目标。在这个大局效果域中,能够履行 JavaScript 代码,可是它没有直接拜访 DOM 的能力,由于 DOM 是主线程的一部分。

WorkerGlobalScope承继了EventTarget,并且完结了一些其他的接口,包含WindowTimers,WindowBase64,WindowEventHandlersGlobalFetch 接口

  1. WindowTimers 接口: WindowTimers 界说了在定时器方面的办法,如 setTimeoutsetInterval。在 Worker 线程中,由于没有 DOM,定时器办法的完结会有所不同,但依然供给了相似的功用。WindowTimers.clearInterval()WindowTimers.clearTimeout()WindowTimers.setInterval()WindowTimers.setTimeout()
  2. WindowBase64 接口: WindowBase64 供给了一些用于处理 Base64 编码的办法。在 Worker 线程中,这样的办法依然能够用于处理数据。WindowBase64.atob()/WindowBase64.btoa()
  3. WindowEventHandlers 接口: WindowEventHandlers 界说了处理事情的办法。尽管 Worker 线程无法直接与 DOM 交互,但它依然能够处理一些与事情相关的操作。
  4. GlobalFetch 接口: GlobalFetch 供给了在大局规模内进行网络恳求的办法,例如 fetch。这答应 Worker 线程进行网络通讯,获取数据等。GlobalFetch.fetch()

这些接口的承继和完结使得 WorkerGlobalScope 具有一些大局效果域应该具有的通用特性,同时也习惯了 Web Worker 的环境。请注意,在 Worker 线程中,并不是一切的 Window 目标的特点和办法都会被完结,由于 Worker 线程中没有 DOM。sessionStoragelocalStorage也是没有办法在WorkerGlobalScope中运用的。在worker中能够运用的浏览器存储有IndexedDB

它具有以下特点以及办法:

  • WorkerGlobalScope.caches(只读目标): 回来与当时上下文相关的CacheStorage目标,它主要与缓存相关,一般用于service worker中。

  • WorkerGlobalScope.navigator(只读目标): 回来与worker相关的 WorkerNavigator它是一个特定的导航器目标,适用worker

  • WorkerGlobalScope.self(只读目标): 回来对 WorkerGlobalScope 本身的引证。大多数情况下,它是一个特定的规模,例如 DedicatedWorkerGlobalScope、SharedWorkerGlobalScope (en-US) 或 ServiceWorkerGlobalScope。

  • WorkerGlobalScope.location(只读目标): 回来与worker相关的 WorkerLocation,Worker 线程的方位信息。与浏览器的主线程不同,Worker 线程中的 location 目标是只读的,且只包含 href 特点,适用于worker

  • WorkerGlobalScope.onerror: 用于设置或获取在 Worker 线程中捕获大局错误的事情处理函数。

  • WorkerGlobalScope.close() 丢掉在 WorkerGlobalScope 的事情循环中排队的任何使命,封闭当时效果域,在 Worker 线程中调用这个办法将会停止该线程

  • WorkerGlobalScope.importScripts()能够动态将多个脚本引入当时worker的上下文中

  • 经过其他接口完结的办法

DedicatedWorkerGlobalScope

DedicatedWorkerGlobalScope 接口表明 Dedicated Worker(专用 Worker)中的大局效果域。这个接口承继自 WorkerGlobalScope,所以包含了与 WorkerGlobalScope 相关的特点和办法。以下是 DedicatedWorkerGlobalScope 特有的特点和办法:

DedicatedWorkerGlobalScope.postMessage() Dedicated Worker 大局效果域中的 postMessage 办法,用于向主线程发送音讯。该办法能够传递多种类型的message的给到外部的worker实例,经过message事情进行监听。

2.3 worker实例目标

了解完worker内部的大局目标后,咱们再来了解一下worker实例。Worker在上文说到过,也是承继于EventTarget所以具有事情目标的相关特点以及办法。除此以外它还具有以下办法以及事情

  • Worker.postMessage() 能够用于跟worker内部的上下文进行通讯,同DedicatedWorkerGlobalScope.postMessage办法参数

  • Worker.terminate() 结束当时worker的行为,不会等待worker完结剩余的操作

  • message 用于接纳来自于worker内部上下文的message。

  • messageerrorDedicatedWorkerGlobalScopemessageerror事情,当worker内给外部实例传递一条无法返序列化的数据是有此报错

  • error 当在worker内履行上下文抛出错误时,会触发当时事情

总结: 尽管 Worker 线程是在浏览器环境中被引发,可是它与当时页面窗口运行在不同的大局上下文中,咱们常用的顶层目标 window,以及 parent 目标在 Worker 线程上下文中是不可用的。它有自己的顶层目标,即self。别的,在 Worker 线程上下文中,操作 DOM 的行为也是不可行的,document目标也不存在。可是,locationnavigator目标能够以可读办法拜访。除此之外,绝大多数 Window 目标上的办法和特点,都被同享到 Worker 上下文大局目标 WorkerGlobalScope 中。

3. worker的运用

3.1 创立 worker

创立 worker 只需求经过 new 调用 Worker() 结构函数即可,它接纳两个参数

const worker = new Worker(path, options);
参数 说明
path 有用的js脚本的地址,必须恪守同源战略。无效的js地址或许违反同源战略,会抛出SECURITY_ERR类型错误
options.type 可选,用以指定worker 类型。该值能够是classicmodule。 如未指定,将运用默认值classic
options.credentials 可选,用以指定 worker 凭证。该值能够是omit,same-origin,或include。假如未指定,或许 type 是classic,将运用默认值omit(不要求凭证)
options.name 可选,在DedicatedWorkerGlobalScope的情况下,用来表明worker 的 scope 的一个DOMString值,主要用于调试目的。

3.2 主线程与 worker 线程数据传递

主线程与 worker 线程都是经过 postMessage 办法来发送音讯,以及监听 message 事情来接纳音讯。如下所示:

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立worker
myWorker.addEventListener('message', e => { // 接纳音讯
    console.log(e.data); // Greeting from Worker.js,worker线程发送的音讯
});
// 这种写法也能够
// myWorker.onmessage = e => { // 接纳音讯
//    console.log(e.data);
// };
myWorker.postMessage('Greeting from Main.js'); // 向 worker 线程发送音讯,对应 worker 线程中的 e.data
// worker.js(worker线程)
self.addEventListener('message', e => { // 接纳到音讯
    console.log(e.data); // Greeting from Main.js,主线程发送的音讯
    self.postMessage('Greeting from Worker.js'); // 向主线程发送音讯
});

好了,一个简单 worker 线程就创立成功了。

postMessage() 办法接纳的参数能够是字符串、目标、数组等。详细咱们在3.7讨论。

主线程与 worker 线程之间的数据传递是传值而不是传地址。所以你会发现,即便你传递的是一个Object,并且被直接传递回来,接纳到的也不是本来的那个值了。

// main.js(主线程)
const myWorker = new Worker('/worker.js');
const obj = {name: '小明'};
myWorker.addEventListener('message', e => { 
    console.log(e.data === obj); // false
});
myWorker.postMessage(obj);
// worker.js(worker线程)
self.addEventListener('message', e => {
    self.postMessage(e.data); // 将接纳到的数据直接回来
});

3.3 监听错误信息

web worker 供给两个事情监听错误,errormessageerror。这两个事情的区别是:

事情 描绘
error 当worker内部出现错误时触发
messageerror message 事情接纳到无法被反序列化的参数时触发

监听办法跟接纳音讯一致:

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立worker
myWorker.addEventListener('error', err => {
    console.log(err.message);
});
myWorker.addEventListener('messageerror', err => {
    console.log(err.message)
});
// worker.js(worker线程)
self.addEventListener('error', err => {
    console.log(err.message);
});
self.addEventListener('messageerror', err => {
    console.log(err.message);
});

3.4 封闭 worker 线程

worker 线程的封闭在主线程和 worker 线程都能进行操作,但对 worker 线程的影响略有不同。

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立worker
myWorker.terminate(); // 封闭worker
// worker.js(worker线程)
self.close(); // 直接履行close办法就ok了

无论是在主线程封闭 worker,仍是在 worker 线程内部封闭 worker,worker 线程当时的 Event Loop 中的使命会继续履行。至于 worker 线程下一个 Event Loop 中的使命,则会被直接忽略,不会继续履行。

区别是,在主线程手动封闭 worker,主线程与 worker 线程之间的连接都会被立刻中止,即便 worker 线程当时的 Event Loop 中仍有待履行的使命继续调用 postMessage() 办法,但主线程不会再接纳到音讯。

在 worker 线程内部封闭 worker,不会直接断开与主线程的连接,而是等 worker 线程当时的 Event Loop 一切使命履行完,再封闭。也就是说,在当时 Event Loop 中继续调用 postMessage() 办法,主线程仍是能经过监听message事情收到音讯的。

举例说明:

在主线程封闭 worker

咱们能够思考一下,主线程会接纳到哪些音讯呢,控制台会打印出哪些信息呢?

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立 worker
myWorker.addEventListener('message', e => {
    console.log(e.data);
    myWorker.terminate(); // 封闭 worker
});
myWorker.postMessage('Greeting from Main.js');
// worker.js(worker线程)
self.addEventListener('message', e => {
    postMessage('Greeting from Worker');
    //settimeput增加一个宏使命
    setTimeout(() => {
        console.log('setTimeout run');
        postMessage('Greeting from SetTimeout');
    });
    //promise增加一个微使命
    Promise.resolve().then(() => {
        console.log('Promise run');
        postMessage('Greeting from Promise');
    })
    for (let i = 0; i < 1001; i++) {
        if (i === 1000) {
            console.log('Loop run');
            postMessage('Greeting from Loop');
        }
    } 
});

运行成果如下:

提高用户体会计划之Web Worker—Worker1

  • 主线程只会接纳到 worker 线程第一次经过 postMessage() 发送的音讯,后边的音讯不会接纳到;
  • worker 线程当时 Event Loop 里的使命会继续履行,包含微使命;
  • worker 线程里 setTimeout 创立的下一个 Event Loop 使命队列没有履行。

在 worker 线程内部封闭 worker

对上述比方稍作修正,将封闭 worker 的事情放到 worker 线程内部,咱们觉得又会打印出什么呢

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立 worker
myWorker.addEventListener('message', e => {
    console.log(e.data);
});
myWorker.postMessage('Greeting from Main.js');
// worker.js(worker线程)
self.addEventListener('message', e => {
    postMessage('Greeting from Worker');
    self.close(); // 封闭 worker
    setTimeout(() => {
        console.log('setTimeout run');
        postMessage('Greeting from SetTimeout');
    });
    Promise.resolve().then(() => {
        console.log('Promise run');
        postMessage('Greeting from Promise');
    })
    for (let i = 0; i < 1001; i++) {
        if (i === 1000) {
            console.log('Loop run');
            postMessage('Greeting from Loop');
        }
    }
});

运行成果如下:

提高用户体会计划之Web Worker—Worker1

与在主线程封闭不同的是,worker 线程当时的 Event Loop 使命队列中的 postMessage() 事情都会被主线程监听到。

3.5 Worker 线程引证其他js文件

总有一些场景,需求放到 worker 进程去处理的使命很杂乱,需求许多的处理逻辑,咱们当然不想把一切代码都塞到 worker.js 里,那样就太糟糕了。web worker 为咱们供给了解决计划,咱们能够在 worker 线程中运用 importScripts() 办法加载咱们需求的js文件,并且,经过此办法加载的js文件不受同源战略束缚

// utils.js
const add = (a, b) => a + b;
// worker.js(worker线程)
// 运用办法:importScripts(path1, path2, ...); 
importScripts('./utils.js');
console.log(add(1, 2)); // log 3

3.6 ESModule 形式

还有一些场景,当你敞开一个新项目,用 importScripts() 导入js文件时发现, importScripts() 办法履行失败。细心一看,发现是新项目的 js 文件都用的是 ESModule 形式。难道要把引证到的文件都改一遍吗?当然不是,还记得上文说到初始化 worker 时的第二个可选参数吗,咱们能够直接运用 module 形式初始化 worker 线程!

// main.js(主线程)
const worker = new Worker('/worker.js', {
    type: 'module'  // 指定 worker.js 的类型
});
// utils.js
export default add = (a, b) => a + b;
// worker.js(worker线程)
import add from './utils.js'; // 导入外部js
self.addEventListener('message', e => { 
    postMessage(e.data);
});
add(1, 2); // log 3
export default self; // 只需把尖端目标self暴露出去即可

3.7 主线程和 worker 线程可传递哪些类型数据

许多场景,在调用某些办法时,咱们将一些自界说办法当作参数传入。可是,当你运用 postMessage() 办法时这么做,将会导致 DATA_CLONE_ERR 错误。

// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创立worker
const fun = () => {};
myWorker.postMessage(fun); // Error:Failed to execute 'postMessage' on 'Worker': ()=>{} could not be cloned.

那么,运用 postMessage() 办法传递音讯,能够传递哪些数据?

postMessage() 传递的数据能够是由结构化克隆算法处理的任何值或 JavaScript 目标,包含循环引证。

结构化克隆算法不能处理的数据:

  • Error以及Function目标;

  • DOM 节点

  • 目标的某些特定参数不会被保留

    • RegExp目标的lastIndex字段不会被保留
    • 特点描绘符,setters 以及 getters(以及其他相似元数据的功用)同样不会被仿制。例如,假如一个目标用特点描绘符标记为 read-only,它将会被仿制为 read-write
    • 原形链上的特点也不会被追踪以及仿制。

结构化克隆算法支撑的数据类型:

4. woker的实践

前面咱们说到了,关于杂乱核算和耗时操作,堵塞主线程操作,能够考虑运用woker来解决。让咱们一起来结合项目实践思考一下详细的运用场景:

场景1:大文件切片上传

思路剖析:

  • step1: 将文件切片,依据文件巨细和每个要切多大核算切多少片,用于切片的下标核算
  • step2 运用FileReader 目标来异步读取文件的分片,
  • step3:运用了 SparkMD5 来核算哈希值,以确保文件的完整性。
  • step4:每个分片读取完结后,经过 Promise 的 resolve 办法回来一个包含分片信息的目标,包含分片的开始方位、结束方位、索引、核算的哈希值以及分片的文件目标。
  • step5:切片完结,拿到一切切片信息,进行上传

那么哪个过程能够运用worker来处理呢

没错。关于切片的核算能够放在worker里处理,也就是step2和step3,处理完后将切片信息放到一个数组里经过postmessage传给主线程,主线程经过onmessage能够拿到一切切片信息。主线程停止worker线程。

你甚至能够拿到用户设备的逻辑处理器中心数量,创立多个并行worker,将切片总量均分到每个worker,进行并行核算,当一切线程处理完结后,再进行主线程的下一步处理。这样能缩短处理时间,更进一步的提高用户体会

提高用户体会计划之Web Worker—Worker1

场景2:用户输入的内容重塑

比方有个需求,给了一系列表单,里面有一个文本框,用户输入大段内容,需求前端依据用户输入的内容进行从头整合,比方款式重绘,辨认特殊文本高亮等。

思路剖析

试想关于超大段的内容处理是不是很费时,很容易形成页面卡顿,这个时分就能够考虑用worker了,把用户的输入value传给worker,在worker里进行各种把戏解析,处理完后传给主线程进行渲染展现,worker工作时不影响用户操作别的表单项。oh,多么丝滑的体会。

场景3:table导出大文件Excel

表格是咱们常常接触的东西,当体系里有table表格的时分,那么它大概率还会伴着导出excel的需求。当咱们扛着40米大刀架到后端脖子上,后端表明:要excel没有,要命一条!ok,要害时分还得靠咱们前端解救世界。

思路剖析

  • step1 :经过exceljs构建表格相关的参数
  • step2:传入相关的数据,然后转换为blob流
  • step3:最终经过file-save导出

假如你仅仅这样吭哧吭哧的做了,那么产品经理必定会举着他们80米的大刀来问你做了个什么玩意儿。好的,坚强的前端仔绝不认输,咱们来优化一下。

首先创立worker线程,经过postmessage向worker线程传递相应的excel数据,在worker线程中经过exceljs构建表格相关数据,然后转换为blob流,接着将生成的blob流经过postmessage传回来主线程,最终经过file-save导出

以上列举了3个场景,希望能带给咱们一些启示,结合自己的事务场景考虑要不要运用。当然这么纯剖析比较笼统,下面让咱们结合详细的demo感受一下worker的魅力。当然也不是任何场景都适合worker的,那么什么场景下不适用呢,由于篇幅原因,详细请移步到下一篇文章-提高用户体会计划之Web Worker—Worker2