背景
众所周知,在浏览器环境中,event-loop存在两种任务:marco-task (宏任物)和 micro-task(微任务)。
且micro-task的优先级要高于marco-task。常见的说法是:浏览器在每一个循环的过程中会先消费完所有的micro-task,然后再取出一个marco-task执行,这被认为是一次事件循环。
疑问
但是,为什么要进行这么设计呢?这个问题似乎很少被讨论。
Micro-task
在浏览器中,主要存在以下两种微任务:
- Promise
- MutationObserver 的回调
本次,我们主要讨论Promise。简单举个例子:
function makeXhrRequest(method, url, data = null) {
return new Promise((resolve, reject) => {
// 创建一个新的 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
// 初始化请求,设置请求方法和请求 URL
xhr.open(method, url, true);
// 设置请求头,如果有数据需要发送,通常将 Content-Type 设置为 application/json
if (method === 'POST' || method === 'PUT') {
xhr.setRequestHeader('Content-Type', 'application/json');
}
// 监听请求状态变化事件
xhr.onreadystatechange = function () {
// 当请求完成(readyState 为 4)
if (xhr.readyState === 4) {
// 如果状态码在 200 到 299 之间,表示请求成功
if (xhr.status >= 200 && xhr.status < 300) {
try {
// 尝试将响应数据解析为 JSON 格式
const responseData = JSON.parse(xhr.responseText);
// 解析成功,将数据作为 Promise 的成功结果返回
resolve(responseData);
} catch (error) {
// 解析失败,将原始响应文本作为成功结果返回
resolve(xhr.responseText);
}
} else {
// 请求失败,将错误信息作为 Promise 的拒绝结果返回
reject(new Error(`Request failed with status: ${xhr.status}`));
}
}
};
// 监听请求错误事件
xhr.onerror = function () {
// 请求发生网络错误,将错误信息作为 Promise 的拒绝结果返回
reject(new Error('Network error occurred'));
};
// 发送请求,如果有数据需要发送,将数据转换为 JSON 字符串发送
xhr.send(data ? JSON.stringify(data) : null);
});
}
// 使用示例
const apiUrl = 'https://jsonplaceholder.typicode.com/posts';
// 发送 GET 请求
makeXhrRequest('GET', apiUrl)
.then(response => {
console.log('Response:', response);
})
.catch(error => {
console.error('Error:', error);
});
在这个例子中,我们简单的使用promise对一个xhr请求进行了封装,并且这个例子也可以代表了我们在大部分的编码场景promise使用的模式,即:Promise通常是对宏任务回掉的封装。而Promise设计的初衷是为了搭配async/await 解决 callback-hell 类似的问题,使异步的处理看起来更加符合人类理解的逻辑。也就是说:微任务通常是由宏任务触发,且逻辑上和触发它的宏任务绑定。
在这个例子中,xhr的state-change事件是一个宏任务,而Promise的resolve是一个微任务。
并发的场景
让我们看看多个异步任务的场景下会发生什么
- 第一次事件循环,任务1执行完,并且产生了一个Promise微任务
- 第二次事件循环,先执行任务1产生的微任务,再执行任务2.
- 从而保证了任务1,任务2的执行顺序符合他们的实际顺序
假设
那么我们假设不存在微任务,Promise也是一种宏任务,会发生什么。
- 第一次事件循环,任务1执行完,并且产生了一个Promise resolve事件,被塞回了事件队列。
- 第二次事件循环,再执行任务2。
- 第三次事件循环,执行任务1产生的promise。
- 因此,导致在我们的业务代码中,任务1的处理晚于任务2。
推理
那么,就可以比较好的推理出我们关于微任务宏任务优先级的疑问了。即微任务的优先级高于宏任务的原因是:为了保证多个宏任物在不同的处理方法(Promise和callback)下的顺序和实际事件的触发顺序是相同且符合逻辑的。
总结
通过上述的分析,我们再来提炼一下关键知识点
- 在浏览器环境中,event-loop存在两种任务:marco-task (宏任物)和 micro-task(微任务)。
- micro-task的优先级要高于marco-task。浏览器在每一个循环的过程中会先消费完所有的micro-task,然后再取出一个marco-task执行,这被认为是一次事件循环。
- 这个现象的原因是为了保证多个宏任物的处理的顺序是和实际事件的触发顺序相同且符合逻辑的。