使用单线程处理好安排的任务
js
function add() {
const a = 1 + 2; // 任务一
const b = 1 + 3; // 任务二
const c = 1 + 4; // 任务三
console.log('a: ', a, ', b: ', b, ', c: ', c); // 任务四
}
add();
在执行这段代码的时候,会把所有的任务都按照顺序写进主线程中,等到线程执行时,这些任务会按照顺序在线程中依次被执行,等到所有任务执行完成之后,线程会自动推出。
在线程运行过程中处理新任务
但并不是所有的任务都是在执行代码的时候都统一安排好的,大部分情况下都会有新的任务在线程运行过程中产生。那当有新任务产生的时候上述方式就无法满足这种情况。
所以为了满足这种在线程运行过程中,能够接收并执行新的任务,就需要采用事件循环机制。
可以通过for循环来监听是否有新的任务。
js
// 获取用户输入的函数
function getInput() {
let inputNumber = 0;
// 通过 prompt 获取用户输入
const userInput = prompt("请输入一个数字:");
// 将用户输入的字符串转换为数字
inputNumber = parseInt(userInput);
// 检查是否成功转换为数字
if (isNaN(inputNumber)) {
console.log("输入无效,请输入一个有效的数字。");
return getInput(); // 递归调用以重新获取有效输入
}
return inputNumber;
}
// 主线程函数
function mainThread() {
for (; ;) {
const firstNumber = getInput();
const secondNumber = getInput();
const resultNum = firstNumber + secondNumber;
// 使用 console.log 输出结果
console.log("和为:", resultNum);
}
}
// 在浏览器中运行时,可以使用 alert 替代 prompt
// function getInput() {
// let inputNumber = 0;
// const userInput = prompt("请输入一个数字:");
// inputNumber = parseInt(userInput);
// if (isNaN(inputNumber)) {
// alert("输入无效,请输入一个有效的数字。");
// return getInput();
// }
// return inputNumber;
// }
// 调用主线程函数
mainThread();
相对于上面的,这一段代码做了如下改进:
第一:引入了循环机制: 具体实现就是在线程语句最后添加了一个for循环语句
,线程会一直循环执行。
第二:引入了事件: 可以在线程运行的时候,等待用户输入的数字,等待过程中线程处于阻塞状态,一旦接收到用户输入的信息,那么线程就会被激活,然后执行相加运算,最后输出结果。
通过引入事件循环机制,就可以让线程"活"起来,我们每次输入两个数字都会打印两个数字相加的结果。
处理其他线程发送过来的任务
上面改进了线程的执行方式,引入了事件循环机制,可以让线程执行的过程中接收新的任务,但是这种模型中,所有的任务都是来自于线程内部,如果另外一个线程想让主线程执行一个任务,这种模型无法做到。
从图中可以看出渲染主进程会频繁收到IO线程的一些任务,接收这些任务之后,渲染主进程就会进行处理。
线程接收其他线程发送的消息的模型:消息队列
消息队列是一种数据结构,可以存放要执行的任务。符合队列"先进先出"的特点,也就是需要添加任务的时候,添加到队列尾部,要取出任务的时候,从队列的头部取出。
从图中可以得知,改造分为三个步骤
- 添加一个消息队列
- IO线程中产生的新任务添加进消息队列尾部
- 渲染主进程会循环的从消息队列头部读取任务,执行任务。
js
// 创建消息队列类
class MessageQueue {
constructor() {
this.queue = [];
}
// 向消息队列尾部添加任务
enqueue(task) {
this.queue.push(task);
}
// 从消息队列头部取出任务
dequeue() {
return this.queue.shift();
}
// 判断消息队列是否为空
isEmpty() {
return this.queue.length === 0;
}
}
// 模拟IO线程产生的任务
function produceTask(messageQueue) {
setInterval(() => {
// 模拟IO线程产生一个任务
const task = () => {
console.log("执行IO线程产生的任务");
};
// 将任务加入消息队列
messageQueue.enqueue(task);
}, 2000); // 假设每两秒产生一个任务
}
// 模拟渲染主进程的事件循环
function eventLoop(messageQueue) {
setInterval(() => {
if (!messageQueue.isEmpty()) {
// 从消息队列取出一个任务
const task = messageQueue.dequeue();
// 执行任务
task();
}
}, 1000); // 假设每秒执行一次事件循环
}
// 创建消息队列实例
const messageQueue = new MessageQueue();
// 启动IO线程模拟任务的产生
produceTask(messageQueue);
// 启动渲染主进程的事件循环
eventLoop(messageQueue);
消息队列中的任务类型
常见的任务类型
- 异步操作任务: 包括从网络获取数据、读取文件、定时器回调等。
- 事件处理任务: 处理用户输入、鼠标事件、键盘事件等。
- UI 更新任务: 更新用户界面的任务,例如在数据变化时重新渲染视图。
- 定时器任务: 通过
setTimeout
、setInterval
等设置的定时任务。 - Promise 回调任务: 处理异步操作完成后的回调。
- Web API 回调任务: 处理诸如
fetch
、XMLHttpRequest
等 Web API 的异步回调。 - 队列操作任务: 对消息队列进行操作的任务,例如添加或删除任务。
- 用户自定义任务: 应用程序中定义的任务,根据具体需求而定。
如何安全退出
当页面主线程执行完成之后,如果确定需要退出当前页面,页面主进程会设置一个退出标志,在每次执行完一个任务的时候都会判断退出标志。
页面使用单线程的缺点
如何处理优先级高的任务
我们把任务又划分成宏任务
和微任务
,我们把消息队列中的任务称为宏任务,每个宏任务中包含了一个微任务队列。
宏任务(Macro Task):
- 定义: 宏任务代表的是一个离散的、完整的工作单元。在浏览器环境中,宏任务可以包括整体的
script
执行、setTimeout、setInterval、I/O
操作等。 - 执行时机: 宏任务会被放入宏任务队列中,在每轮事件循环中执行一个宏任务。当前宏任务执行完成后,才会去执行下一个宏任务。
微任务(Micro Task):
- 定义: 微任务通常来说是一个需要异步执行的较小的任务单元。在浏览器环境中,微任务包括
Promise
的回调、MutationObserver
回调等。 - 执行时机: 微任务会被放入微任务队列中,在当前宏任务执行完毕、下一个宏任务开始执行之前,将微任务队列中的所有微任务执行完毕。
执行流程:
- 执行宏任务: 事件循环开始时,执行当前宏任务队列中的第一个宏任务。
- 执行微任务: 在执行完当前宏任务后,检查微任务队列,将微任务队列中的所有微任务依次执行完毕。
- 更新渲染: 浏览器进行渲染,更新页面显示。
- 重复: 重复上述过程,形成一个事件循环。
这样的设计有助于更细致地控制任务的执行顺序,确保某些任务在其他任务之前或之后执行。例如,Promise
的回调就是微任务,会在当前任务结束后、下一个任务开始前执行,这有助于处理异步代码,而不影响页面的渲染和响应。
单个任务执行时长过久
因为所有任务都是在单线程中执行的,所以每次都只能执行一个任务,其他任务就都处于等待状态,如果一个任务执行过久,那么下一个任务就需要等待很长事件,就会造成页面卡顿现象。
Chrome 采用的优化策略:
- 任务分片: Chrome 使用
任务分片(Task Scheduling)
的方式,将长时间运行的任务划分为多个小片段。这样可以让其他任务在分片任务之间插入执行,从而提高页面的响应性。 - requestAnimationFrame: 长时间运行的任务可以通过
requestAnimationFrame
进行分片,将任务拆分成多个帧中执行,确保每一帧都有时间执行渲染和用户交互。 - Web Workers:
Chrome
支持Web Workers
,允许在后台线程中执行计算密集型任务,避免阻塞主线程。这有助于保持页面的响应性,特别是对于一些需要大量计算的任务。 - Idle Callback:
Chrome
支持requestIdleCallback
,该 API 允许在浏览器空闲时执行任务。这可以用于执行一些不紧急但耗时的任务,而不影响页面的实时交互。 - 事件循环的微任务:
Chrome
使用事件循环机制,通过微任务队列来执行异步任务。Promise
的回调、async/await
中的任务都属于微任务,会在宏任务执行结束后立即执行,确保异步任务及时执行。 - Timeouts 和 Intervals 优化:
Chrome
对setTimeout
和setInterval
进行了优化,确保它们的执行不会过早或过晚。Chrome
会将定时器的回调任务放入合适的宏任务队列中,以保证执行的准确性。 - DevTools 的性能分析:
Chrome
提供强大的开发者工具,包括性能分析工具,可以帮助开发者检测性能问题,分析任务执行时间,并定位潜在的性能瓶颈。