单线程语言的事件循环Event Loop
- 事件循环机制确保了宏任务和微任务的顺序执行,保证了 JavaScript 的非阻塞特性。
首先需要理清:
JavaScript是单线程的,意思是JavaScript的执行时只有一个主线程 ,这部分详见单线程JavaScript的异步编程
宿主环境(node、浏览器)是多线程的。Web Worker 主要用于浏览器环境,允许在后台线程中执行 JavaScript,而不干扰 UI 线程。
Worker Thread 在 Node.js 环境中使用,提供了一种在多线程中执行 JavaScript 的方式,适用于需要大量计算或并行处理的任务。
两者都利用了多线程的概念来提高应用程序的性能和响应能力,但是它们的使用场景和实现细节有所不同。
主线程是单线程,所有同步任务都在主线程上执行,形成一个执行栈(execution context stack).所有阻塞的部分交给一个线程池处理,然后主线程通过事务队列跟线程池协作。
非阻塞是指需要执行异步任务时候,主线程会pending这个任务,当异步任务执行完毕会被放到事务队列中。事务队列在当前执行栈中的所有任务执行完毕后执行。
基于事件循环:主线程的执行过程就是一个 tick(同步、微任务、宏任务),不断循环
异步任务队列其实有两种,微任务和宏任务。先执行所有的微任务再宏
在 JavaScript 中,任务(Tasks)和微任务(Microtasks)是执行代码的方式,它们与事件循环(Event Loop)紧密相关,共同管理 JavaScript 运行时的行为。
不同运行环境、比如node、不同浏览器都会有区别。
以下是微任务和宏任务的基本概念和区别:
宏任务(Macrotasks)
宏任务通常是由浏览器定时执行的任务,它们包括但不限于:
- 定时器(
setTimeout
和setInterval
) - 网络请求的回调
- 资源加载完成(如图片、样式表等)
- UI 渲染(浏览器将计算后的结果绘制到屏幕上)
宏任务在执行时会查看队列中的所有任务,一次执行一个任务,然后进行 UI 渲染,接着再次检查队列。
requestAnimationFrame 是一种特殊的宏任务,它告诉浏览器你想要在下一个重绘(repaint)之前执行某些动画相关的代码。浏览器会在执行这个函数时尽量同步到垂直同步(VSync),也就是屏幕的刷新率,通常是每秒 60 次刷新,这意味着 requestAnimationFrame 的回调会在屏幕每一次刷新之前执行,从而实现平滑的动画效果。
由于 requestAnimationFrame 的回调是在浏览器的重绘阶段执行,它实际上是在当前宏任务结束后,下一个微任务队列开始之前执行的。然而,它仍然被认为是一个宏任务,因为它遵循宏任务的执行模式:一次执行一个任务,然后进行 UI 渲染。
总结来说,requestAnimationFrame 是宏任务的一种,但它的执行时机与普通的宏任务(如 setTimeout)不同,它是在浏览器的重绘阶段执行,以实现更平滑的动画效果。
微任务(Microtasks)
微任务通常是由 JavaScript 运行时内部的任务,它们包括但不限于:
Promise
回调MutationObserver
的回调queueMicrotask
微任务在当前宏任务执行完成后立即执行,如果存在多个微任务,它们会按照队列中的顺序依次执行,直到队列为空。
区别
-
执行时机:
- 宏任务在当前任务执行完毕后,检查队列并执行下一个宏任务,然后进行 UI 渲染。
- 微任务在当前宏任务执行完毕后,立即执行所有待处理的微任务,然后再执行下一个宏任务。
-
优先级:
- 微任务的优先级高于宏任务。在执行下一个宏任务之前,所有的微任务都会先执行完毕。
-
用途:
- 宏任务常用于不需要立即执行,但需要周期性或延迟执行的场景。
- 微任务常用于需要尽快执行,且不阻塞 UI 更新的场景。
示例
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
上述代码的输出顺序将是:
Script start
Script end
promise1
promise2
setTimeout
try{
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
await Promise.reject().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
}).then(function() {
console.log('promise3');
}).finally(function() {
console.log('promise4');
});
console.log('Script end');}
catch(e){
console.error(11,e)}
上述代码的输出顺序将是:
Script start
VM4440:15 promise4
VM4440:20 11 undefined
window.console.error @ app-index.js:33
console.error @ hydration-error-info.js:63
VM4440:5 setTimeout
示例
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
// 异步操作
try {
// 模拟异步操作
setTimeout(() => {
console.log('timing in promise');
resolve('成功');
}, 0);
} catch (error) {
reject('失败');
}
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
上述代码的输出顺序将是:
Script start
VM4452:24 Script end
undefined
VM4452:4 setTimeout
VM4452:12 timing in promise
VM4452:19 promise1
VM4452:21 promise2
console.log('Script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve, reject) => {
// 异步操作
try {
resolve('成功');
} catch (error) {
reject('失败');
}
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('Script end');
上述代码的输出顺序将是:
Script start
VM4463:20 Script end
VM4463:15 promise1
VM4463:17 promise2
undefined
VM4463:4 setTimeout
这是因为 setTimeout
是一个宏任务,而 Promise
回调是微任务。微任务在当前脚本(也是一个宏任务)执行结束后立即执行,而 setTimeout
则被安排在下一个宏任务队列中。
理解微任务和宏任务的概念对于编写高效且可预测的 JavaScript 代码非常重要。
queueMicrotask
queueMicrotask
是一个相对较新的 Web API,它允许你在当前宏任务的末尾,下一个宏任务开始之前,将一个函数排队为微任务执行。这意味着使用 queueMicrotask
排队的函数将在当前运行栈清空后,所有已有的微任务队列执行完毕后,但在下一个宏任务开始之前执行。
特性和用途:
-
快速执行:
queueMicrotask
允许你快速执行那些需要尽早处理但又不想通过事件循环再次循环的任务。 -
避免延迟:相比
Promise
的.then()
,queueMicrotask
可以避免额外的事件循环迭代,因为它将在当前栈清空后立即执行。 -
适用于非阻塞操作:如果你有一些不涉及 I/O 操作的代码需要执行,使用
queueMicrotask
可以确保它们在不影响用户界面的情况下尽快执行。 -
不返回值或接受参数:
queueMicrotask
接受一个函数作为参数,该函数在执行时不接受任何参数,也不返回任何值。 -
示例用法:
console.log('Script start');
Promise.resolve().then(() => {
console.log('microtask: promise');
});
queueMicrotask(() => {
console.log('microtask: queueMicrotask');
});
console.log('Script end');
上述代码的输出顺序将是:
Script start
Script end
microtask: queueMicrotask
microtask: promise
在这个例子中,尽管 queueMicrotask
的函数是在 Promise
回调之前定义的,但它将在 Promise
回调之前执行,因为 queueMicrotask
会在当前宏任务的所有微任务队列清空后立即执行。
注意事项:
-
queueMicrotask
并不是所有浏览器都支持,但在现代浏览器中(如 Chrome、Firefox)已经得到支持。 -
过度使用
queueMicrotask
可能会导致性能问题,尤其是当大量微任务连续排队时,它们会延迟下一个宏任务的执行。 -
queueMicrotask
可以与Promise
结合使用,以优化性能,特别是在处理大量异步操作时。 -
在某些情况下,
queueMicrotask
可以作为requestAnimationFrame
的替代方案,尤其是在不需要与屏幕刷新率同步的情况下。
queueMicrotask
提供了一种优化 JavaScript 执行顺序的手段,使得开发者可以更精细地控制异步代码的执行时机。
Axios 本质上是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js 环境中。它不是宏任务,而是使用 Promise 来处理 HTTP 请求的异步响应。
Axios 和 Promise:
- Promise 基础:Axios 的请求返回一个 Promise 对象。这意味着你可以使用
.then()
方法来处理成功的响应,使用.catch()
方法来处理错误。 - 链式调用:由于基于 Promise,Axios 支持链式调用,可以连续处理多个
.then()
或.catch()
。 - async/await 支持:Axios 与
async/await
语法兼容,这使得你可以以更同步的方式编写异步代码。
示例代码:
// 使用 Promise
axios.get('/user')
.then(response => {
console.log('请求成功', response.data);
})
.catch(error => {
console.error('请求失败', error);
});
// 使用 async/await
async function getUser() {
try {
const response = await axios.get('/user');
console.log('请求成功', response.data);
} catch (error) {
console.error('请求失败', error);
}
}
Axios 和宏任务:
- 非阻塞:Axios 的 HTTP 请求不会阻塞 JavaScript 的主线程,因为它们在后台异步执行。
- 宏任务特性:虽然 Axios 本身不是宏任务,但它的请求处理是作为宏任务的一部分执行的。例如,在浏览器中,网络请求通常作为宏任务处理,而响应回调则在事件循环的下一个迭代中执行。
总结:
Axios 是一个基于 Promise 的 HTTP 客户端,用于执行异步的 HTTP 请求。它利用了 JavaScript 的异步编程特性,特别是 Promise,来处理请求和响应。Axios 的请求和响应处理遵循 JavaScript 的事件循环和宏任务/微任务队列,但它本身并不定义为宏任务。宏任务通常指的是浏览器或 Node.js 环境中的高级别异步操作,如定时器、网络请求等,而 Axios 作为这些操作的封装,其内部实现细节对用户来说是透明的。
性能影响:
- 过度使用微任务(如大量
Promise
回调)可能导致性能问题,因为它们会在当前宏任务结束后连续执行,可能导致长时间占用 CPU,从而影响 UI 渲染。
了解微任务(Microtasks)和宏任务(Macrotasks)的异步队列对于前端开发的性能优化至关重要。这些概念帮助开发者理解 JavaScript 的事件循环机制,从而编写更高效、更响应的代码。以下是一些性能优化的启示:
-
避免长时间运行的任务:
- 长时间的同步任务会阻塞主线程,影响 UI 更新和用户体验。尽量将复杂的计算分解或使用 Web Workers。
-
合理使用异步编程:
- 使用
Promise
、async/await
等异步编程技术可以避免阻塞主线程,同时使代码更易于理解和维护。
- 使用
-
优化微任务的使用:
- 微任务在当前宏任务完成后立即执行,可以在微任务中执行快速的、不影响 DOM 更新的逻辑。
-
合理安排宏任务:
- 宏任务如
setTimeout
、网络请求等,可以合理安排在事件循环中的位置,避免在关键时刻阻塞渲染。
- 宏任务如
-
避免不必要的重排和重绘:
- 批量 DOM 操作和使用
DocumentFragment
可以减少重排和重绘的次数,从而提高性能。
- 批量 DOM 操作和使用
-
利用 requestAnimationFrame 优化动画:
requestAnimationFrame
可以确保动画在浏览器的下一次重绘之前更新,提供平滑的动画效果。
-
合理使用事件委托:
- 事件委托可以减少事件监听器的数量,通过在父元素上监听事件来处理子元素的事件,提高性能。
-
避免在事件处理程序中执行重任务:
- 事件处理程序如点击、滚动等可能会频繁触发,避免在这些处理程序中执行复杂的计算或同步的 I/O 操作。
-
使用防抖和节流技术:
- 对于频繁触发的事件(如滚动、窗口调整大小等),使用防抖(debounce)或节流(throttle)技术限制事件处理的频率。
防抖函数:接受一个func,设定一个顶层变量t,初值为空,返回一个函数,函数内部,用t接受settimeout的注册的返回值,如果t不为空,用t取消clearTimeout(t);对应注册。
- 对于频繁触发的事件(如滚动、窗口调整大小等),使用防抖(debounce)或节流(throttle)技术限制事件处理的频率。
function debounce(fn, wait) {
let timeoutId = null;
return function(...args) {
const context = this;
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
const handleResize = () => {
console.log('窗口大小改变');
};
window.addEventListener('resize', debounce(handleResize, 200));
节流:接受一个func,设定一个顶层变量isDone,初值为false,返回一个函数,函数内部,isDone ===false时,设置isDone true,并执行函数,settimeout注册定时设置isDone =false
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const handleScroll = () => {
console.log('滚动事件');
};
window.addEventListener('scroll', throttle(handleScroll, 100));
-
优化资源加载:
- 按需加载资源和代码分割可以减少初始加载时间,提高页面响应速度。
-
使用服务工作者(Service Workers):
- Service Workers 可以提供离线支持、缓存控制和网络请求拦截,有助于提高应用性能。
-
监控性能指标:
- 使用浏览器的开发者工具监控性能指标,如帧率(FPS)、CPU 和内存使用情况,以便发现并解决性能瓶颈。
-
异步数据加载:
- 对于数据密集型应用,使用懒加载和异步数据加载技术,只在需要时才加载数据。
-
避免过多的微任务积累:
- 虽然微任务可以快速执行,但过多的微任务积累可能导致性能问题,应注意控制微任务的数量和执行时间。
通过深入理解微任务和宏任务的机制,开发者可以编写出更高效的 JavaScript 代码,优化前端应用的性能,提供更流畅的用户体验。