前言: JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
最近在做vue的项目中,遇到了计算量庞大导致页面响应缓慢的问题,正好每个计算任务的结果不需要汇总,所以想到了以多线程的形式去执行每个计算任务。但是通过一轮google、baidu都没有找到vue的案例,最后发现web worker可以做多线程。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
Web Worker 有以下几个使用注意点:
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document
、window
、parent
这些对象。但是,Worker 线程可以navigator
对象和location
对象。
(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker 线程不能执行alert()
方法和confirm()
方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://
),它所加载的脚本,必须来自网络。
介绍了这么多的web worker 的知识,不如来点案例看看。
案例:
正常我们洗澡后才洗衣服对吧,js执行的话也只能一步步执行,先干啥,在干啥。
1、洗完澡后再洗衣服: 在没有洗衣机(Worker)前,通常都是先洗完澡后再手洗衣服,这样我们每天都要花费不少时间在洗澡和洗衣服上,花费时长为:洗澡所用时长+洗衣服所用时长
。相当于下面这段代码:
javascript
console.time('一共花了多少时长')
console.time('洗澡所用时长')
let length_1 = 300000000;
let sum1 = 0
for (let i = 0; i <= length_1; i++) {
sum1 += i
}
console.log('%c循环1执行完:' + sum1, 'color:green')
console.timeEnd('洗澡所用时长')
console.time('洗衣服所用时长')
let length_2 = 200000000;
let sum2 = 0
for (let i = 0; i <= length_2; i++) {
sum2 += i
}
console.log('%c循环2执行完:' + sum2, 'color:green')
console.timeEnd('洗衣服所用时长')
console.timeEnd('一共花了多少时长')
打印结果:
(从结果可以看到大概第2.7秒循环1执行完了,接着大概到了第4.5秒后循环2才执行完,所以从中可以看出循环1阻塞了循环2,所以此时花费时长为:循环1+循环2)
2、把衣服放到洗衣机后洗澡: 当我们拥有洗衣机(Worker)后,就可以把衣服放到洗衣机后愉快的洗澡了,此时为异步操作,那么这时花费的总时长取决于谁更后洗完了,花费时长为:Math.max(洗澡所用时长,洗衣服所用时长)
。相当于下面的代码:
现在本地建个文件夹 里面放 :
|-index.html
|-worker.js
index.html:
html
<!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>Web Worker</title>
</head>
<body>
</body>
<script>
console.time('洗衣机洗完衣服了,所用时长')
let worker = new Worker("./worker.js"); // 开启副线程
let length_1 = 300000000;
let sum1 = 0
worker.postMessage(length_1); // 发送消息给副线程
worker.onmessage = e => { // 监听副线程返回的消息
sum1 = e.data
console.log('%c循环1执行完了:' + e.data, 'color:green')
console.timeEnd('洗衣机洗完衣服了,所用时长')
worker.terminate() // 关闭线程
}
console.time('洗完澡了,所用时长')
let length_2 = 200000000;
let sum2 = 0
for (let i = 0; i <= length_2; i++) {
sum2 += i
}
console.log('%c循环2执行完了:' + sum2, 'color:green')
console.timeEnd('洗完澡了,所用时长')
</script>
</html>
worker.js:
javascript
self.onmessage = function (e) { //监听主线程发过来的消息
let length_1 = e.data
let sum = 0
for (let i = 0; i <= length_1; i++) {
sum += i
}
self.postMessage(sum); // 将信息发送到主线程上
}
从结果可以看到大概第1.9秒循环2执行完了,紧接着接着大概第2.4秒后循环1也执行完了,当我们把循环1放到线程上执行时并没有阻塞后续的循环2,因为循环2循环时长短所以先打印出来了,因此此时花费总时长为:循环1。
注: 由于谷歌浏览器不支持读取本地文件,浏览器出于安全考虑,不允许直接从file:///
协议加载Web Worker脚本。这是因为本地文件系统路径(如file:///
)可能被恶意软件利用来执行不受信任的代码。
解决方法:
1、 设置允许访问本地文件
只需要右键谷歌浏览器的快捷方式,查看属性,在目标一栏中空出一格 然后加入字符串**--allow-file-access-from-files
**,先点应用,再点击确定即可。
2、 允许跨域请求
建议再创建一个新的谷歌浏览器的快捷方式,与之前的快捷方式区分开,在想使用跨域请求的时候打开新的快捷方式,保证上网安全。
先在磁盘上创建一个无关紧要的文件夹,你应该也可以指定一个已经存在的文件夹,复制文件夹的路径,在目标一栏中加入--user-data-dir="YourDirectory" --disable-web-security,注意把YourDirectory替换成你新建或者指定的目录。点击保存后打开快捷方式,如何出现下图提示,则表示设置成功,就可以进行跨域请求了。
然后就是设置: 你需要开发用的文件夹目录的路径,
然后把你上一步添加的目标位置的 字符串,换成这个(里面含有必须要读取的文件夹目录位置)
javascript
--allow-file-access-from-files --user-data-dir="C:\你需要读取的文件夹的名字"
总结:最后虽然折腾这么久后只省了几秒钟,但是当项目计算量越来越大,多线程的优势就会变得特变明显了。