背了一堆干巴巴的概念,一个都用不上,不知道他是干嘛?背了忘,忘了背,我在干嘛呀!不开心。
那今天我们体验下不一样的 EventLoop 呗!
一.基本概念
1.进程和线程
你打开chrome浏览器以后,在任务管理器里面吗,你看到只有八个进程,分别是浏览器主进程,GPU进程,插件进程,渲染进程、网络进程等这几个。
粗略来看是一个标签页对应一个渲染进程,共用一个浏览器主进程。
在渲染进程里面只有一个主线程,来完成html解析,js执行,css计算,页面布局,绘制等工作。
这也说明了,为什么存在事件循环机制的原因了,就是因为他一个人一次性只能干一件,干了这个干不了那个,要想快速干完所有的事情,就必须给那些事情指定一个规则,把他们按照优先级,排个序,然后再一个一个执行。这就是:事件循环机制(eventLoop)。
其实事件循环机制你可以理解成是任务循环机制,因为它内部维护的就是一个个的task
2.任务
浏览器将它所干的事情划分成一个个的task,然后把这些 task 放到任务队列里面,一个一个执行,而这些任务在执行的时候,必须得划分优先级,所以在浏览器期初,将任务划分成了宏任务和延迟任务,其实延迟任务就是宏任务的另一种表现形式。
但是后来,由于页面变得越来越复杂,浏览器想要宏任务产生的其他任务早点执行,但是又不能放到延迟任务里面去,因为延迟任务的执行时间并不准确,一旦线程阻塞,就会出现延时问题。
所以为了解决这些冲突,就出现了promise,它相当于是在宏任务里面又维护了一个微任务队列,每个宏任务都有一个微任务队列,当宏任务执行的时候,他会把所有产生的微任务都放在这个队列里面去,当宏任务里面的事情干完之后,它才会去执行微任务队列里面的事情,直到所有微任务执行完毕,才会去执行下一个宏任务。
我们用案例来证明下面这些结论:
一个script标签,会开启一个单独的task。
一个定时器,会开启一个单独的task。
一个事件,会开启一个单独的task。
微任务不开启task,它归属于它所在的宏任务里面。
具体是不是,就看看下面这四个例子:
1.1 一个script产生一个task
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function a (){
let sum1 = 0
for(let i = 0; i < 30000000; i++){
sum1 += i
}
console.log(sum1);
return sum1
}
const sum1 = a();
console.log('window下面的', sum1)
</script>
<script>
function a (){
let sum = 0
for(let i = 0; i < 30000000; i++){
sum += i
}
console.log(sum);
return sum
}
const sum = a();
console.log('window下面的', sum)
</script>
</body>
</html>
1.2 一个定时器开启一个task
比如这段代码:一个函数,一个定时器,产生2个task
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function a (){
let sum = 0
for(let i = 0; i < 30000000; i++){
sum += i
}
console.log(sum);
}
a();
setTimeout(()=>{
a()
},0)
</script>
</body>
</html>
你看到了吗?延迟任务是宏任务,它单独开启了一个task。
1.3 一个事件开启一个task
在这个script标签里面,如果你有点击事件,都是放在一个task里面的。
比如:
<script>
function a (){
let sum = 0
for(let i = 0; i < 30000000; i++){
sum += i
}
console.log(sum);
return sum
}
const sum = a();
console.log('window下面的', sum)
setTimeout(()=>{
const sum = a();
console.log('window下setTimeout的', sum)
},0)
const btn = document.getElementById("btn");
btn.onclick = function(){
const sum = a();
console.log('按钮里面的', sum)
}
</script>
我的代码产生了三个task,一个是全局作用下执行的a,一个是定时器里面的a,还有一个btn执行的a
1.4 点击事件产生延迟也是一个task
那如果在同步任务里面产生了延迟任务呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">点我呀</button>
<script>
function a (){
let sum = 0
for(let i = 0; i < 30000000; i++){
sum += i
}
console.log(sum);
return sum
}
const sum = a();
console.log('window下面的', sum)
setTimeout(()=>{
const sum = a();
console.log('window下setTimeout的', sum)
},0)
const btn = document.getElementById("btn");
btn.onclick = function(){
const sum = a();
console.log('按钮里面的', sum)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
},0)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
},12)
}
</script>
</body>
</html>
结果:
产生了五个task,也印证了一件事,就是一个setTimeout是一个task,一个事件是一个task。(不管你这个事件是直接onclick的还是用addEventListener监听的。)
1.5点击事件里面产生微任务
const btn = document.getElementById("btn");
btn.onclick = function(){
const sum = a();
console.log('按钮里面的', sum)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
},0)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
},12)
new Promise((resolve)=>{
const sum = a();
console.log('按钮产生的微任务构造里面的', sum)
resolve(sum)
}).then((res)=>{
const sum = a();
console.log('按钮产生的微任务then产生的', sum)
console.log('按钮产生的微任务then接受到的', sum)
})
}
上面这段代码只产生了三个task,后面2个是延迟任务的,微任务是谁产生的就在谁的task里面。
1.6 延迟任务产生个微任务
const btn = document.getElementById("btn");
btn.onclick = function(){
const sum = a();
console.log('按钮里面的', sum)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
new Promise((resolve)=>{
const sum = a();
console.log('按钮产生的微任务构造里面的', sum)
resolve(sum)
}).then((res)=>{
const sum = a();
console.log('按钮产生的微任务then产生的', sum)
console.log('按钮产生的微任务then接受到的', sum)
})
},0)
setTimeout(()=>{
const sum = a();
console.log('按钮产生的定时器的', sum)
},12)
new Promise((resolve)=>{
const sum = a();
console.log('按钮产生的微任务构造里面的', sum)
resolve(sum)
}).then((res)=>{
const sum = a();
console.log('按钮产生的微任务then产生的', sum)
console.log('按钮产生的微任务then接受到的', sum)
})
这个案例也印证了,谁产生的微任务就放在谁的task里面。
如果宏任务里面产生了另一个宏任务怎么办?那就再开启一个task呗!
总结
宏任务(macrotask):代表一个离散的工作单元,script,setTimeout,setInterval,setImmediate,I/O,UI-rendering(ui渲染)
微任务(microtask):比宏任务更小的任务单元,promise.then(),MutationObserver。微任务会在当前宏任务代码执行完毕后,去执行他自己的微任务,只有它自己的微任务全部执行完毕以后,才会开启下一个宏任务。
二.performance如何监测性能
单独学习什么EventLoop肯定越学越蒙,那借助performance呢,你都不用再背诵概念了吧!真的起到了事半功倍的效果。
1.performance介绍
打开开发者工具,进入performance,点击reload录制
其中 Main 这部分就是网页的主线程,也就是执行 Event Loop 的部分:
鼠标划到想看的部分,滚动鼠标,或者向下拖动,就可以放大那个区域:
我们把这个花花绿绿的图叫做火焰图,不过他是向下燃烧的。
在这里我们只需要记住它的几个颜色代表就好了,如果你记不住,也没有关系,点击图会有介绍,比如下面
但是为了你能快速分辨出来它到底是谁的task,我劝你还是记一下下面这个总结,因为你看到的大部分线条都是浏览器自己的task,不是你自己的task
灰色就代表宏任务 task
橙色的是浏览器内部的 JS
蓝色的是 html 的 parse
紫色是样式的 reflow、repaint
绿色的部分就是渲染
其余的颜色都是用户 JS 的执行了
react的调用栈
至于宏任务微任务是怎么执行的,在任务里面我就说的很清楚了,不再赘述。
2.优化目标长任务
其实上面已经说清楚了task,也简单的了解了proformance下面的task,现在我们就看看proformance怎么做性能优化?
为什么要做性能分析?不就是因为页面卡么!
那么多代码,我咋知道是谁在卡,总不可能打个console.log(11)就把页面卡死了吧,所以说要想看数据呀,就要看看谁的task有异常,那么什么样子的task有异常的?
上面这个task,是不是异常了,人家都给你标红了,熟悉不?那我们就点开看看它的特性吧
它明确的告诉你这是一个长任务,一般显示器的刷屏频率是16.7HZ,也就是每60ms刷新一次,如果你的task执行时间超过50ms,我们就定义为你这个任务是长任务。
说人话就是本来我60ms刷新一次,页面就要变动了,但是你还没有执行完,我不得等你,我就等呀等,页面白呀白,刷了也没有东西显示,所以就白屏呗。
现在优化目标已经明确了,就是找长任务,那找到长任务以后,我怎么知道它到底哪里耗时了?
点这个文件你就可以看到具体哪个文件在作妖。
三.优化手段Web Worker
文档https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers
1.基础说明
Web Worker是在后台运行的脚本,不会影响用户界面,因为它在单独的线程中运行,而不是在主线程中。
之前就说了,渲染进程里面只有一个线程,干所有渲染相关的事情,现在我们用new Worker开启另外一个线程,干我们自己定义的事情。你可能说了,既然能定义线程,为啥渲染线程还是单线程的,咋不多搞几个,不就快了么!想法很美好,或许未来可以实现呀!
它限制肯定是有各种原因的,因为当你开启worker的时候,他也是一对一的,一次只能开一个,用完以后立即销毁。限制还是比较大的。
由于我们在项目开发时,使用不同的打包工具(vite/webpack)。幸运的是,最新版的vite/webpack都支持Web Worker了。
文档如下:
2.webpack/vite的支持
我们可以用new URL()的方式 --vite/webpack都支持
new Worker(
new URL(
'./worker.js',
import.meta.url
)
);
也可以用import 方式 只有vite支持
import MyWorker from './worker?worker'
const worker = new MyWorker()
3.基本使用
在index.html里面引入new Worker(),但是一定要注意的是:new Worker的入参是一个url,且一定是一个url。
相当于就是两个文件,我创建worker实例以后,用postMessage发送出去一个消息,然后对应的文件用 addEventListener('message',()=》{})监听到消息,监听到以后,他会做具体的处理,处理完以后,再用postMessage发送出来处理结果,这边worker接受,然后做其他处理,弄完以后销毁worker。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title> worker performance optimization</title>
</head>
<body>
<script>
console.log('线程之前')
const worker= new Worker('./work.js');
worker.postMessage(200);
worker.addEventListener('message', function (evt) {
console.log(evt.data)
});
worker.onerror = ()=>{
console.log('error')
};
worker.terminate()//销毁
</script>
</body>
</html>
worker.js
addEventListener('message', function (evt) {
let total = 0;
let num = evt.data;
for (let i = 0; i < num; i++) {
total += i;
}
postMessage(total);
});
4. Blob 方式
其实也是上面的变体,之前不是说Worker的构造函数的参数一定是个url么,如果我要在本文件里面使用work呢?也就是new Worker和他的执行提都在一个文件里面,咋办?
其实,我们要知道一个道理,内存里面的东西都是二进制,二进制就有对应的内存地址,你还记得下载文件吗?先把文件存在浏览器对应的内存里,然后用window.URL.createObjectURL()拿到内存地址,然后再用img的src=url展示出来。
其实,现在也一样,我们可以把执行函数所在的内存地址拿到,然后把这个地址丢给new Worker(),这样线程就建立好了。
// 定义要在Worker中执行的脚本内容
const workerScript = `
self.onmessage = function(e) {
console.log('来自主线程的消息: ' + e.data);
self.postMessage('向主线程发送消息: ' + 'Hello, ' + e.data);
};
`;
// 创建一个Blob对象,指定脚本内容和类型
const blob = new Blob(
[workerScript],
{ type: 'application/javascript' }
);
// 使用URL.createObjectURL()方法创建一个URL,用于生成Worker
const blobURL = URL.createObjectURL(blob);
// 生成一个新的Worker
const worker = new Worker(blobURL);
// 监听来自Worker的消息
worker.onmessage = function(e) {
console.log('来自worker的消息: ' + e.data);
};
// 向Worker发送消息
worker.postMessage('Front789');
你觉得写到字符串末班里面太烦人了,你也可以把他写到script标签里面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web Worker</title>
</head>
<body>
<script id="worker" type="app/worker">
console.log(self);
self.onmessage = function(event) {
console.log('子线程收到消息:', event.data);
self.postMessage('get✔');
}
self.onerror = function (err) {
console.log('子线程异常:', err);
}
throw new Error('test error');
</script>
<script>
let blob = new Blob([document.querySelector('#worker').textContent]);
let url = window.URL.createObjectURL(blob);
let worker = new Worker(url);
worker.onmessage = function(event) {
console.log('主线程收到消息:', event.data);
}
worker.onerror = function (err) {
console.log('主线程收到子线程异常:', err);
}
worker.postMessage('Hello World');
</script>
</body>
</html>
type是个乱七八糟的东西,浏览器不认识,所以不解析,其实内核都一样的,一个发消息,一个处理,处理完之后再把结果返回给他就ok了。
5.在vue里面使用
在/src/worker.js
//
/**
* 计算斐波那契数
* @param {number} n
* @returns {number} 返回第 n 个斐波那契数
*/
function fib(n) {
let arr = [0,1];
for (let i = 2; i <= n; i++) {
arr[i] = arr[i-1] + arr[i-2];
}
return arr[n];
}
self.onmessage = function (e) {
let n = e.data;
self.postMessage(fib(n));
self.close();
}
在组件里面
如果你的react项目是vite搭建的,所以你完全可以使用上面这种做法。如果你是webpack的,那你得试试原始的办法了。
6.在react里面使用worker
如果你在react的webpack项目里面,建立一个work.js,然后直接new Worker('./worker.js');就会报错
报错的主要原因是react文件会编译,编译打包以后,地址都会发生变化,除非你在打包的时候能够把work.js单独给分出来。
正确使用如下,先创建一个worker.js
const workerCode = () => {
onmessage = (e) => {
// 打印从主线程发送的消息
console.log(e.data);
postMessage('woker收到回复');
};
};
export default workerCode;
在组件里面
import React, { useEffect } from 'react';
import workerCode from './worker'; // 导入 Worker 脚本
const APP: React.FC = () => {
useEffect(() => {
const workerUrl = URL.createObjectURL(
new Blob([`(${workerCode})()`], { type: 'application/javascript' }),
);
const myWorker = new Worker(workerUrl);
myWorker.postMessage('主线程发送信息');
myWorker.onmessage = function (event) {
// 接收 Worker 发送的消息
console.log(event.data);
};
return () => {
myWorker.terminate();
URL.revokeObjectURL(workerUrl);
};
}, []);
return <div>React组件内容</div>;
};
export default APP;
你会说,弄了半天,这不是bolb使用法么,是的,他就是的!
7. @koale/useWorker
useWorker是一个轻量级的React Hooks库,专为充分利用Web Workers而设计。它封装了一个简单的API,允许开发者在React组件中无缝地使用Web Workers处理后台任务。只需几行代码,即可实现复杂的计算任务,而不会影响用户的交互体验。
优点:
- 无UI阻塞:通过Web Worker,在后台执行耗时任务,确保UI流畅。
- Promise 风格:使用Promise进行通讯,使得异步编程更直观。
- 轻量级:体积小于3KB,对项目影响极小。
- TypeScript 支持:提供类型定义,增强开发体验。
- 自动管理:自动创建和销毁Web Worker,避免资源浪费。
- 远程依赖支持:可以加载外部脚本到Web Worker中使用。
- 超时控制:可设定任务执行超时选项。
npm install --save @koale/useworker
使用
import React from 'react';
import { useWorker } from '@koale/useworker';
const numbers = [...Array(5000)].map(() => (Math.random() * 1000));
const sortNumbers = nums => nums.sort();
const Example = () => {
const [sortWorker] = useWorker(sortNumbers);
const runSort = async () => {
const result = await sortWorker(numbers);
console.log(result);
};
return (
<button type='button' onClick={runSort}>
Run Sort
</button>
);
};
export default Example;
用起来好优雅呀,是不是?
8.自己封装
靠天靠地不如靠自己,不管怎么样,其实还得自己有货才行,对不对?
hook/useWorker.jsx
import React from 'react';
const createWorkerCode = (code) => {
return (`
onmessage = async (e) => {
const __worker_run = ${code}
const __worker_result = await __worker_run(e.data);
postMessage(__worker_result);
};
`);
};
const useWorker = (code, config = {}) => {
const {
params,
closeWorkerOnUnmount = true, // hooks卸载时是否结束worker
manual = false, // 是否手动执行worker
} = config;
const [data, setData] = React.useState();
const content = createWorkerCode(code);
console.log(content, 555);
const url = URL.createObjectURL(new Blob([content], { type: 'text/javascript' }));
const worker = new Worker(url);
React.useEffect(() => {
worker.onmessage = (e) => { // 监听worker接收事件,然后更新result
setData(e.data);
};
return () => {
closeWorkerOnUnmount && worker.terminate(); // hooks卸载时是否结束worker
};
}, [code]);
React.useEffect(() => {
!manual && worker.postMessage(params); // 执行worker文件并给worker传参
}, [JSON.stringify(params)]);
// 手动执行worker逻辑
const run = (p) => {
worker.postMessage(p);
};
return {
data, // worker执行的结果
run, // 手动执行worker
worker, // worker实例
};
};
export default useWorker;
使用
import React from 'react';
import useWorker from '../hooks/useWorker';
const workerCode = (nums) => {
return nums.sort();
};
const numbers = [...Array(5000)].map(() => (Math.random() * 1000));
const Worker = () => {
const { data, run } = useWorker(workerCode, { manual: true });
console.log('排序结果:', data);
return (
<button onClick={() => run(numbers)}>大数据排序</button>
);
};
export default Worker;
其实说来说去就是在封装blob方式