前言
在JavaScript
中,任务被分为同步任务和异步任务。
- 同步任务:这些任务在主线程上顺序执行,不会进入任务队列,而是直接在主线程上排队等待执行。每个同步任务都会阻塞后续任务的执行,直到它自身完成。常见的同步任务包括页面的初始化、DOM操作和某些计算任务。
- 异步任务 :与同步任务不同,异步任务不直接进入主线程执行,而是被放入任务队列(
task queue
)中。只有当主线程空闲时,才会从任务队列中取出任务来执行。异步任务不会阻塞主线程的执行。根据任务类型,异步任务又被分为宏任务 和微任务。
一、事件循环是什么?
JavaScript
的事件循环(Event Loop
)是其运行时环境(如浏览器或Node.js
)处理异步操作和回调的一种机制 。它允许JavaScript
在不阻塞单线程执行的情况下,响应用户交互、处理网络请求、定时器回调等异步事件。
二、事件循环的基本基本概念和工作原理
1. 调用栈(Call Stack
):
JavaScript
引擎有一个调用栈,用于跟踪函数调用的顺序。当一个函数调用发生时,它会被推入调用栈中。当函数执行完毕后,它会被从调用栈中弹出。
2. 任务队列(Task Queue
):
当异步操作(如 setTimeout、setInterval、DOM 事件、Promise.resolve().then()
等)完成时,它们会生成一个任务(task
)(简单理解为任务就是回调函数),并将这个任务放入相应的任务队列中等待执行。
- 这些任务队列包括宏任务队列(
macrotask queue
)和微任务队列(microtask queue
) - 宏任务队列主要存放
script
(全局任务)、setTimeout
、setInterval
、setImmediate
(Node.js
环境)等;微任务队列主要存放Promise
的回调函数、MutationObserver
(浏览器环境)等。
3. 事件循环:
事件循环的基本顺序是:
- 当调用栈为空时(即没有正在执行的函数),事件循环会查看任务队列。
- 事件循环会率先查看微任务队列。如果微任务队列中有任务,它会将任务逐个取出并执行,直到微任务队列为空。
- 然后,事件循环会查看宏任务队列。如果宏任务队列中有任务,它会将任务取出并执行。在执行宏任务的过程中,可能会产生新的微任务,这些微任务会被添加到微任务队列的末尾。
- 当一个宏任务执行完毕后,事件循环会再次查看微任务队列并执行其中的任务,这个过程会一直重复,直到所有的任务都被执行完毕。
- 这个过程会持续进行,形成了一个循环,这就是所谓的"事件循环"。
三、宏任务和微任务?
- 在
JavaScript
的事件循环中,任务的执行被分为两种主要的类别:宏任务(MacroTask
)和微任务(MicroTask
)。这两种任务类型在事件循环中的处理顺序和方式有所不同。
1. 宏任务(MacroTask
)
宏任务通常包括:
script
(整体代码)setTimeout
setInterval
setImmediate
(Node.js
环境)I/O
UI
渲染(浏览器会在每次事件循环结束后进行UI渲染)MessageChannel
(消息通道)postMessage
(一些HTML5 API
使用)requestAnimationFrame
(浏览器用于定时执行动画)
宏任务创建后会被放入宏任务队列中,JavaScript引擎会在当前执行栈清空后,从宏任务队列中取出队首任务执行。
2. 微任务(MicroTask
)
微任务通常包括:
Promise.then()
或Promise.catch()
MutationObserver
(HTML5
的API
,用于监听DOM
变更)process.nextTick
(Node.js
环境)
与宏任务不同,微任务是在当前宏任务执行完成后立即执行的。在JavaScript
引擎执行完一个宏任务后,它会先查看微任务队列,并执行所有的微任务,直到微任务队列为空。然后,它会继续取出并执行下一个宏任务。这个过程会不断重复,形成JavaScript
的事件循环。
执行顺序
考虑以下的示例:
js
javascript
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'); // 同步任务
尽管setTimeout的延迟被设置为0,但它的回调仍然会在所有的微任务之后执行。因此,上述代码的输出顺序为:
script start
script end
promise1
promise2
setTimeout
这是因为当JavaScript引擎执行到setTimeout时,它会将回调函数放入宏任务队列,并继续执行后续的代码。当执行到Promise.then()时,它会将回调函数放入微任务队列。在所有宏任务代码执行完毕后,JavaScript引擎会先执行所有的微任务,然后再从宏任务队列中取出并执行下一个宏任务。
四、练习
- 练习一:
js
console.log('Start'); // 同步任务
// 宏任务1
setTimeout(() => {
console.log('Timeout callback'); // 同步任务
Promise.resolve().then(() => {
console.log('Promise 1'); // 微任务1
Promise.resolve().then(() => {
console.log('Promise 2'); // 微任务2
Promise.resolve().then(() => {
console.log('Promise 3'); // 微任务3执行完执行下一个宏任务
});
});
});
}, 0);
// 宏任务2
setTimeout(() => {
console.log('Timeout222 callback'); // 6
}, 0);
- 练习二:
js
// 开启一个微任务,当dom修改时触发
const observer = new MutationObserver(function (mutationsList, observer) {
console.log(mutationsList, observer)
});
const config = { attributes: true, childList: true, subtree: true };
console.log('script start'); // 同步任务 1
(function () {
console.log('自执行函数 '); // 同步任务 2
})()
// 宏任务2
setTimeout(function () {
Promise.resolve().then(function () {
var element = document.getElementById('app');
observer.observe(element, config);
var child = document.getElementById('child');
element.innerHTML = '<p>这是一段新的HTML内容。</p>';
console.log('promise11'); // 同步任务
Promise.resolve().then(() => {
console.log('promise11 callback 1'); // (3) 微任务
});
Promise.resolve().then(() => {
console.log('promise11 callback 2'); // (3) 微任务
});
})
console.log('setTimeout'); // 同步任务
}, 0);
// 宏任务3
setTimeout(() => {
console.log(111);
})
Promise.resolve().then(function () {
console.log('promise1'); // 微任务1
Promise.resolve().then(() => {
console.log('promise1 callback 1'); // 微任务1-2
});
Promise.resolve().then(() => {
console.log('promise1 callback 2'); // 微任务 1-3
});
}).then(function () {
console.log('promise2'); // 微任务2
// 宏任务4
setTimeout(() => {
console.log('微任务内的宏任务'); // 宏任务队列4
Promise.resolve().then(() => {
console.log('微任务2 promise callback'); // 微任务队列4
});
})
Promise.resolve().then(() => {
console.log('promise2 callback 1'); // 微任务2-1
});
Promise.resolve().then(() => {
console.log('promise2 callback 2'); // 微任务2-2
});
})
console.log('script end'); // 同步任务3
// 执行同步任务,当遇到异步宏任务放入宏任务队列,异步微任务放入微任务队列
// 所以执行顺序
// script start
// 自执行函数
// script end
// promise1
// promise1 callback 1
// promise1 callback 2
// promise2
// promise2 callback 1
// promise2 callback 2
// ---宏任务2
// setTimeout
// promise11
// MutationObserver
// promise11 callback 1
// promise11 callback 2
// ---宏任务3
// 111
// ---宏任务4
// 微任务内的宏任务
// 微任务2 promise callback
- 练习三:
script
整体为何是宏任务
js
// 宏任务一
<script>
console.log('script1') // 同步
// 宏任务三
setTimeout(() => {
console.log('setTimeout1');
// 宏任务五
setTimeout(() => {
console.log('setTimeout3');
})
})
// 微任务
Promise.resolve().then(() => {
console.log('promise1');
})
</script>
// 宏任务二
<script>
console.log('script2') // 同步
// 宏任务四
setTimeout(() => {
console.log('setTimeout2');
// 宏任务六
setTimeout(() => {
console.log('setTimeout4');
})
})
// 微任务
Promise.resolve().then(() => {
console.log('promise2');
})
</script>
可以看出来script相当于setTimeOut开启宏任务列表,执行完当前宏任务去执行微任务,微任务执行完毕,执行宏任务二,以此类推
所以输出结果:
script1
promise1
script2
promise2
setTimeout1
setTimeout2
setTimeout3
setTimeout4