前言
一天一个知识点,今天我们聊聊web worker。
一.什么是Web worker? 以及Web work的由来
当我们有时候访问并操作一些存在大量数据的页面时,有时候浏览器会弹出这样一个提示框。
这表示当前页面“冻结了”,“卡顿了”,甚至“网页崩溃了、脚本无响应”。
如果你足够了解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 的两个限制:
-
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
-
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'] });
它们相互之间的通信可以传递对象和数组.
值得注意的是:它们之间通信是通过拷贝的形式来传递数据的,进行传递的对象需要经过序列化,接下来在另一端还需要反序列化。这就意味着
- 我们不能传递不能被序列化的数据,比如函数,会抛出错误的。
- 在一端改变数据,另外一端不会受影响,因为数据不存在引用,是拷贝过来的。
监听 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');
-
脚本中的全局变量都能被 worker 线程使用。
-
脚本的下载顺序是不固定的,但执行时会按照传入 importScripts() 中的文件名顺序进行,这个过程是同步的。
多个 worker 线程
-
在主线程内可以创建多个 worker 线程。
-
worker 线程内还可以新建 worker 线程,使用同源的脚本文件创建。在 worker 线程内再新建 worker 线程就不能使用window.URL.createObjectURL(blob),需要使用同源的脚本文件来创建新的 worker 线程,因为我们无法访问到window对象。
线程间转移二进制数据
因为主线程与 worker 线程之间的通信是拷贝关系,当我们要传递一个巨大的二进制文件给 worker 线程处理时(worker 线程就是用来干这个的),这时候使用拷贝的方式来传递数据,无疑会造成性能问题。
幸运的是,Web Worker 提供了一中转移数据的方式,允许主线程把二进制数据直接转移给子线程。这种方式比原先拷贝的方式,有巨大的性能提升。
一旦数据转移到其他线程,原先线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面