阅读 240

一天一个知识点之Web worker

前言

一天一个知识点,今天我们聊聊web worker。

一.什么是Web worker? 以及Web work的由来

当我们有时候访问并操作一些存在大量数据的页面时,有时候浏览器会弹出这样一个提示框。

image.png

这表示当前页面“冻结了”,“卡顿了”,甚至“网页崩溃了、脚本无响应”。

如果你足够了解JavaScript这门语言的活,联系前面的场景,“大量数据”,我相信你可以大致猜出网页崩溃背后的原因。

单线程,CPU高密度任务,阻塞UI

作为一门单线程语言,JavaScript有着天然的缺点,那就是同一时间只能做同一件事,前面的代码没执行完,那么后面的就需要一直处于等待状态,尽管我们可以使用setTimeOut,setInterval等异步方法模拟并行,减轻主线程的压力,但是这他们都不是真正意义上的并行。

一旦浏览器在执行CPU高密度任务,比如操作成千上万行的表格时,是很容易造成浏览器页面的崩溃的。

这也是为什么HTML5新标准中会引入Web Worker的原因。 这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。

这在很大程度上利用了现在不断升级的电脑计算能力:能够在同一时间平行处理两个任务。

简而言之: Web Worker是HTML新标准中引入的规范,定义了一套API,允许一段JavaScript程序允许在主线程之外的另外一个线程中, Web worker使得网页中进行多线程编程成为可能。 当主线程在处理界面事件时,worker 可以在后台运行,帮你处理大量的数据计算,当计算完成,将计算结果返回给主线程,由主线程更新 DOM 元素。

二.Web worker的限制

无法访问DOM元素,document,window

很容易理解,想象JavaScript被设计成单线程的原因就知道了,如果有多个页面可以操作DOM,那么很有可能在操作同一个DOM不同的线程出现冲突,我们也称之为了race condition

无法访问LocalStorage

和对 dom 元素的限制一样,因为读写 LocalStorage 是同步的,一定会引起 race condition

Web Worker 不支持跨域

无法和主线程共享内存、worker 之间也无法共享内存,所以无需保护数据

这些限制听上去都挺严格的,但是其实都是出于安全而这样设计的,想象一下,如果多个线程都在试着更新同一个元素,那简直就是个灾难。

三. Web worker的使用

主线程

1.通过调用new worker创建worker线程

2.通过worker.postMessage向worker传输信息

3.通过worker.onmessage监听worker传回的信息

4.通过worder.onerror监听错误

5.通过worker.terminate()关闭线程

创建 worker 对象

主线程调用new Worker()构造函数,新建一个 worker 线程,构造函数的参数是一个 url,生成这个 url 的方法有两种

脚本文件
const worker = new Worker('https://~.js');
复制代码

因为 worker 的两个限制:

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

  2. worker 不能读取本地的文件(不能打开本机的文件系统file://),它所加载的脚本必须来自网络。

可以看到限制还是比较多的,如果要使用这种形式的话,在项目中推荐把文件放在静态文件夹中,打包的时候直接拷贝进去,这样我们就可以拿到固定的链接了。

字符串形式
const data = ` 
// worker线程 do something `; 
// 转成二进制对象 
const blob = new Blob([data]); // 生成url 
const url = window.URL.createObjectURL(blob); // 加载url 
const worker = new Worker(url);
复制代码

主线程与 worker 线程通信:

worker.postMessage({ hello: ['hello', 'world'] });
复制代码

它们相互之间的通信可以传递对象和数组.

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

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

监听 worker 线程返回的信息

worker.onmessage = function (e) { 
console.log('父进程接收的数据:', e.data); 
// doSomething(); 
}
复制代码

主线程关闭 worker 线程

Worker 线程一旦新建成功,就会始终运行,这样有利于随时响应主线程的通信。

这也是 Worker 比较耗费计算机的计算资源(CPU)的原因,一旦使用完毕,就应该关闭 worker 线程。

worker.terminate()
复制代码

监听错误

// worker线程报错 
worker.onerror = e => { 
// e.filename - 发生错误的脚本文件名;
e.lineno - 出现错误的行号;
以及 e.message - 可读性良好的错误消息 
console.log('onerror', e);
};
复制代码

worker线程

1.通过self.onmessage监听主线程传过来的信息

2.通过self.postMessage向主线程发送信息

3.通过self.close()关闭自身

worker 线程的执行上下文是一个叫做WorkerGlobalScope的东西跟主线程的上下文(window)不一样。

我们可以使用self/WorkerGlobalScope来访问全局对象。

监听主线程传过来的信息

self.onmessage = e => { 
console.log('主线程传来的信息:', e.data); 
// do something 
};
复制代码

发送信息给主线程

self.postMessage({ hello: [ '这条信息', '来自worker线程' ] });
复制代码

worker 线程关闭自身

self.close()
复制代码

work线程加载脚本

Worker 线程能够访问一个全局函数 imprtScripts()来引入脚本,该函数接受 0 个或者多个 URI 作为参数。

importScripts('http~.js','http~2.js');
复制代码
  1. 脚本中的全局变量都能被 worker 线程使用。

  2. 脚本的下载顺序是不固定的,但执行时会按照传入 importScripts() 中的文件名顺序进行,这个过程是同步的。

多个 worker 线程

  1. 在主线程内可以创建多个 worker 线程。

  2. worker 线程内还可以新建 worker 线程,使用同源的脚本文件创建。在 worker 线程内再新建 worker 线程就不能使用window.URL.createObjectURL(blob),需要使用同源的脚本文件来创建新的 worker 线程,因为我们无法访问到window对象。

线程间转移二进制数据

因为主线程与 worker 线程之间的通信是拷贝关系,当我们要传递一个巨大的二进制文件给 worker 线程处理时(worker 线程就是用来干这个的),这时候使用拷贝的方式来传递数据,无疑会造成性能问题。

幸运的是,Web Worker 提供了一中转移数据的方式,允许主线程把二进制数据直接转移给子线程。这种方式比原先拷贝的方式,有巨大的性能提升。

一旦数据转移到其他线程,原先线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面

文章分类
前端
文章标签