运营:别再让你的页面一直loading 了
运营:别再让你的页面一直loading 了
第一轮 battle
Q: 我想下载一个大文件,界面一直转圈,很耽误时间
,我想在下载的时候还做点其他事情
A:一直转圈就一直等呗,反正还能摸会(奈何小姐姐太想做牛马了)
第二轮 battle
Q: 不行,为什么别人的浏览器,下载软件/文件 就能操作界面,你这就一直转圈,什么都做不了
A: 我们js 是单线程,一个时间只能做一件事,你不能在下载文件的时候,还操作界面吧...逐渐语无伦次,行,我给你试着优化优化..
最终效果
可以看到,下载文件 页面不再转圈,并且可以在界面操作,但是在点击操作1,2,到3的时候,会卡顿一下
,下面会说为什么会卡这一下
开始分析
- 执行文件下载操作,把转圈逻辑去掉不就行了,
but: 是不转圈了,下载的时候,依然操作不了界面
- js 是一个单线程,一个时间只能做一件事,密集的cpu 计算,导致网站反应迟钝,就像卡了一样
resolve: 把下载文件这个耗时操作,放在其他线程操作,等到操作完毕,再通知主线程,执行完了。就像发布订阅模式一样,主线程不用执行密集的计算,也不用特意等密集计算的结果,执行完,告诉我就行了
技术使用 Web Workers
摘自 MDN developer.mozilla.org/zh-CN/docs/…
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面
。此外,它们可以使用 XMLHttpRequest
(尽管 responseXML
和 channel
属性总是为空)或 fetch
(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。
为什么要用它:worker 的一个优势在于能够执行处理器密集型的运算
而
不会阻塞 UI 线程
不会阻塞 UI 线程
不会阻塞 UI 线程
不会阻塞 UI 线程
重要的事情说三遍 🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣
基本使用
主线程生成一个专用 worker
const myWorker = new Worker("worker.js"); // worker.js 是一个脚本的 URI 来执行 worker 线程
专用 worker 中消息的接收和发送
就俩主要方法 postMessage
onmessage
引入脚本与库
Worker 线程能够访问一个全局函数 importScripts()
来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:
importScripts(); /* 什么都不引入 */
importScripts("foo.js"); /* 只引入 "foo.js" */
importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */
ESModule 模式
const worker = new Worker('worker.js',
{ type: 'module' // 指定 worker.js 的类型 }
);
文件下载代码
- baseCode
import { writeFile, utils } from 'xlsx'
/**模拟生成大文件数据 */
const generateLargeFileData = () => {
const data = []
for (let i = 0; i < 10000; i++) {
data.push({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
age: Math.floor(Math.random() * 100) + 1
})
}
return data
}
- 一只转圈的代码
/**下载大文件 */
const downloadExcel = async () => {
// 模拟生成大文件数据
const data = generateLargeFileData()
loading.value = true
// 模拟一段短暂的等待时间,确保状态更新
await delay(1000)
// 卡死的罪魁祸者
// 将数据转换为 Excel 格式
const ws = utils.json_to_sheet(data)
const wb = utils.book_new()
utils.book_append_sheet(wb, ws, 'Sheet1')
writeFile(wb, 'test.xlsx')
loading.value = false
}
- 使用webworker,将耗时计算放到 webworker 线程,解决阻塞ui的问题
主线程
const myWorker = new Worker('downloadWorker.js')
myWorker.onmessage = (event) => {
let wb = event.data
// 这里也会占用主线程的ui渲染,所以会卡一下
writeFile(wb, 'test.xlsx')
ElMessage.success('下载任务已在后台运行,可以继续操作界面其他任务')
}
/**下载大文件 */
const downloadExcel = async () => {
const data = generateLargeFileData()
myWorker.postMessage(data)
}
worker 线程
// 非模块化文件, public 打包本身就是线上文件了
importScripts("./xlsx.js"); // 线上地址,或者本地地址
self.onmessage = (e) => {
// 将数据转换为 Excel 格式
const ws = XLSX.utils.json_to_sheet(e.data)
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
// writeFile(wb, 'test.xlsx') // 这里会操作dom, 所以将操作dom放到 主线程做
self.postMessage(wb)
self.close()
}
细节补充
- 本文主要介绍了专用worker,其实还有 共享 worker【主要做多页面标签通信】, ServiceWorkers 【主要做网络拦截,可以看一下之前写的pwa文章【https://juejin.cn/post/7062681470116036616】,离线缓存就是使用ServiceWorkers】
- 在主线程中使用时,
onmessage
和postMessage()
必须挂在worker
对象上,而在 worker 中使用时不用这样做。原因是,在 worker 内部,worker 是有效的全局作用域(就像window.xxx ,window 一般可以不写) - worker的关闭
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
myWorker.terminate(); // 关闭worker
// worker.js(worker线程)
self.close(); // 直接执行close方法就ok了
- worker 错误监听
messageerror
- 关于主线程里的 new Worker('downloadWorker.js')
这个脚本,必须是本地/或者网络地址,这里写的是项目运行地址 匹配相应的worker。这里大家也会发现一个问题,就是这个worker是全局性的,放在public 是一个不错的选择,再者打包后,public 下本身也是会放在服务器上
- 用完worker, 要及时关闭,他是不会自己结束的。选择 在worker 关闭,或者主线程关闭,会有区别
- 其实小文件下载,用worker 有点画蛇添足,本身使用worker 也是一种消耗
详细的参考资料以及代码地址
MDN
MDN code仓库
可以下载下来直接调试,最好是起一个本地服务: http-server
代码地址
来源:juejin.cn/post/7369633749418934335