JavaScript 单线程特性
JavaScript 是一门单线程的语言,这意味着同一时刻只能执行一个任务。让同步任务尽快执行,进行渲染页面,优先响应用户的交互。并且为了出于实际的考虑,避免了多线程编程中的复杂同步问题,如死锁、竞态条件等
JavaScript 中的任务被分为两大类:
- 同步任务(Synchronous Tasks):立即执行的代码,按照顺序依次执行
- 异步任务(Asynchronous Tasks):不会立即执行,而是被放入任务队列中等待执行
同步任务与页面渲染
在 JavaScript 的执行过程中,所有同步任务会被优先、尽快地执行完毕。只有当这些同步任务全部执行完后,浏览器才会进行页面的渲染工作,包括"重绘"(repaint)和"重排"(reflow)。
为什么要这样做?
因为 JavaScript 是单线程的,如果在执行同步任务时就去渲染页面,可能会导致页面渲染多次,导致效率低下。浏览器会等到所有同步任务执行完毕后,再统一进行一次页面渲染,这样可以提升性能和用户体验。
事件循环机制
JavaScript有宏任务队列和微任务队列,两个队列都遵循先进先出(FIFO)的原则。每次事件循环时进行以下步骤:
- 先执行一个宏任务(包括同步代码)
- 然后清空所有微任务队列
- 微任务队列为空后,浏览器进行渲染
- 最后再进入下一个宏任务
如下图演示:
任务类型
宏任务队列(Macro Task): 可以理解为耗时性的任务,其包括以下任务:
setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、UI rendering 和script(整体代码)。
微任务(Micro Task): 可以理解为紧急的或者优先的任务,其包括以下任务:Promise.then() 、 catch() 、 finally()、MutationObserver、queueMicrotask()和process.nextTick()(Node.js)。
不多说,上代码!
基本事件循环代码
console.log("script start");
// 异步任务,也是宏任务
setTimeout(() => {
console.log("setTimeout");
}, 0); // 虽然为0,但是不会立即执行
// then 异步 微任务
Promise.resolve().then(() => {
console.log("promise");
});
console.log("script end");
一个面试的坑就是:Promise.then()才是微任务,如果你回答了Promise,面试官可能会说:回去等通知吧。
小知识: Promise.resolve()本身是同步代码,它会立即返回一个Promise对象,但是,通过then、catch、finally等方法注册的回调函数是异步执行的(属于微任务)。
代码执行的输出顺序:
script start→script end→promise→页面渲染→setTimeout
微任务
MutationObserver
MutationObserver 是一个微任务,用于监听 DOM 变化:
const target = document.createElement("div");
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log("微任务:MutationObserver");
});
// 监听target 节点的变化
observer.observe(target, {
attributes: true,
});
target.setAttribute("data-set", "123");
target.setAttribute("style", "background-color:green");
批量 DOM 操作
const target = document.createElement("div");
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log("微任务:MutationObserver");
});
// 监听target 节点的变化
observer.observe(target, {
attributes: true,
childList: true,
});
target.setAttribute("data-set", "123");
target.appendChild(document.createElement("div"));
target.setAttribute("style", "background-color:green");
添加 childList: true和target.appendChild(document.createElement("div")) 后,我们发现控制台仍然只输出了一个。这说明所有 DOM 变化被合并成一次微任务回调,并且 DOM 的改变在页面渲染前,我们就能够拿到 DOM 有什么改变。
Node.js 环境下的微任务
process.nextTick 优先级
在 Node.js 环境中,process.nextTick 的优先级比 Promise 更高:
Promise.resolve().then(() => {
console.log('promise1');
process.nextTick(() => {
console.log('nextTick in promise');
});
});
process.nextTick(() => {
console.log('nextTick1');
});
console.log('end');
输出顺序:
end
nextTick1
promise1
nextTick in promise
在 promise1 里注册的 process.nextTick,会立即插队到下一个 nextTick 队列,所以 nextTick in promise 也会在下一个微任务前执行
混合任务
console.log("Start");
// node 微任务
process.nextTick(() => {
console.log("Process Next Tick");
});
// 微任务
Promise.resolve().then(() => {
console.log("Promise Resolved");
});
// 宏任务
setTimeout(() => {
console.log("haha");
Promise.resolve().then(() => {
console.log("inner Promise");
});
}, 0);
console.log("end");
输出顺序:
Start
end
Process Next Tick
Promise Resolved
开始新的一轮轮询
haha
inner Promise
浏览器渲染机制
queueMicrotask 与渲染
console.log("同步");
// 批量更新
// dom 树, cssom,layout 树 图层合并
queueMicrotask(() => {
// DOM 更新了,但不是渲染完了
// 一个元素的高度 offsetHeight scrollTop getBoundingClientRect()
// 立即重绘重排 耗性能
console.log("微任务:queueMicrotask");
});
console.log("同步结束");
执行顺序
- 输出"同步"
- 注册一个微任务(不会立刻执行)
- 输出"同步结束"
- 当前宏任务(同步代码)全部执行完毕后,立刻执行微任务,输出"微任务:queueMicrotask"
- 微任务队列清空后,浏览器才会进行页面渲染
批量渲染优化
浏览器会把 DOM 树、CSSOM、布局树等的更新合并起来,批量处理,减少渲染次数,提高性能。怎么理解?浏览器在执行 JavaScript 时,不会因为DOM或CSS每次的修改就立刻去渲染页面,而是把这些修改"暂存"起来,等到合适的时机(比如本轮宏任务和所有微任务都执行完)再统一进行页面的渲染(重排和重绘)。
事件循环总结
执行顺序规则
- 同步代码:立即执行
- 微任务:在当前宏任务执行完毕后立即执行
- 宏任务:等待下一轮事件循环
- 页面渲染:在所有微任务执行完毕后进行
优先级
- 同步任务 > 微任务 > 页面渲染 > 宏任务
实际开发中的应用场景
性能优化
// 使用微任务进行批量DOM操作
function batchDOMUpdate() {
const elements = document.querySelectorAll(".item");
// 使用微任务确保DOM操作在下一个渲染周期前完成
queueMicrotask(() => {
elements.forEach((el) => {
el.classList.add("updated");
});
});
}
异步操作控制
// 确保异步操作的执行顺序
async function sequentialOperations() {
console.log("开始操作");
// 使用 await 确保顺序执行
await new Promise((resolve) => {
setTimeout(() => {
console.log("操作1完成");
resolve();
}, 1000);
});
await new Promise((resolve) => {
setTimeout(() => {
console.log("操作2完成");
resolve();
}, 1000);
});
console.log("所有操作完成");
}
避免阻塞 UI
// 将耗时操作分解为多个微任务
function processLargeData(data) {
const chunkSize = 1000;
let index = 0;
function processChunk() {
const chunk = data.slice(index, index + chunkSize);
// 处理数据块
chunk.forEach((item) => {
// 处理逻辑
});
index += chunkSize;
if (index < data.length) {
// 使用微任务继续处理下一块
queueMicrotask(processChunk);
}
}
processChunk();
}
总结:JavaScript 事件循环是理解异步编程的核心概念。通过掌握同步任务、微任务、宏任务的执行顺序,以及浏览器渲染机制,可以编写出更加高效、响应迅速的应用程序。在实际开发中,合理利用事件循环机制可以显著提升用户体验和程序性能。