- Published on
태스크큐 & 마이크로태스크큐
태스크 큐
태스크큐란, 실행해야 할 태스크의 집합을 의미한다. 이벤트루프는 태스크큐를 한개 이상 가지고 있고 이름과는 다르게 자료구조에서의 큐(queue)가 아니고 set의 형태를 가지고 있다. 선택된 큐 중에 실행 가능한 가장 오래된 태스크를 가져와야하기 때문이다. 태스크 큐에서의 '실행해야 할 태스크'라는 것은 비동기 함수의 콜백함수나, 이벤트 핸들러 등을 의미한다. 이벤트 루프는 현재 실행 중인 작업이 완료되면 태스크 큐에서 다음 작업을 꺼내어 실행한다.
마이크로 태스크 큐
이벤트 루프는 하나의 마이크로 태스크 큐를 갖는다. 태스크 큐보다 먼저 실행되며, 대표적으로 Promise가 있다. 마이크로 태스크 큐가 빌 때까지 기존 태스크 큐의 실행은 뒤로 미뤄 진다. (태스크큐보다 우선순위가 높다) 마이크로태스크는 현재 실행 중인 태스크가 종료되면 즉시 실행된다. 현재 태스크가 마이크로태스크 큐에 있는 모든 작업을 처리한 후 다음 태스크로 넘어간다.
- 태스크 큐: setTimeout, setInterval, setImmediate
- 마이크로 태스크 큐: process.nextTick, Promise, queueMicroTask, MutationObserver
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<ul>
<li>동기코드: <button id="sync">0</button></li>
<li>태스크: <button id="task">0</button></li>
<li>마이크로 태스크: <button id="microtask">0</button></li>
</ul>
<button id="button">모두 동시 실행</button>
</body>
<script>
const sync = document.getElementById('sync');
const task = document.getElementById('task');
const microtask = document.getElementById('microtask');
const allRun = document.getElementById('button');
sync.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i;
}
});
task.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
setTimeout(() => {
task.innerHTML = i;
}, 0);
}
});
microtask.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
queueMicrotask(() => {
microtask.innerHTML = i;
});
}
});
allRun.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i;
setTimeout(() => {
task.innerHTML = i;
}, 0);
queueMicrotask(() => {
microtask.innerHTML = i;
});
}
});
</script>
</html>
- 동기코드는 0 ~ 100,000까지 숫자가 올라가기 전까지 렌더링이 일어나지 않다가 for문이 끝나야 한번에 렌더링이 된다.
- 태스크큐는 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1 ~ 100,000까지 다 끝난 후에 순차적으로 렌더링된다.
- 마이크로태스크큐는 100,000까지 다 끝낸 후에 렌더링이 된다.
콜백함수나 이벤트 핸들러를 일시 저장한다는 점은 태스크큐와 동일하지만 마이크로태스크큐는 태스크큐보다 우선순위가 높다. 이벤트 루프는 콜 스택이 비면 먼저 마이크로태스크 큐에서 대기하고 있는 함수를 가져와 실행한다. 이후 마이크로태스크 큐가 비면 태스크 큐에서 대기하고 있는 함수를 가져와서 실행한다.
중요한 차이점 우선순위: 마이크로태스크가 태스크큐보다 우선순위가 높기 때문에 마이크로태스크가 모두 처리된 이후에 태스크가 실행됨.
재귀적 호출: 마이크로태스크에서 다른 마이크로태스크를 호출하면 즉시 실행됨. 반면 태스크큐에서 다른 태스크를 호출하면 현재 태스크가 완료된 이후 실행됨.
- 마이크로태스크에서 다른 마이크로태스크 호출 (즉시 실행)
console.log('Start');
Promise.resolve().then(() => {
console.log('First microtask');
// 새로운 마이크로태스크를 호출하고, 이는 즉시 실행됨
Promise.resolve().then(() => {
console.log('Nested microtask');
});
console.log('End of first microtask');
});
console.log('End');
// ------
// 결과
// Start
// End
// First microtask
// End of first microtask
// Nested microtask
위 코드에서 첫 번째 마이크로태스크에서 다른 마이크로태스크를 호출했을 때, 호출된 마이크로태스크는 현재 마이크로태스크가 종료되자마자 즉시 실행.
- 태스크 큐에서 다른 태스크 호출 (완료 이후 실행)
console.log('Start');
// 태스크 큐에 등록된 콜백 함수
setTimeout(() => {
console.log('Task in the task queue');
}, 0);
console.log('End');
//------
// 결과
// Start
// End
// Task in the task queue
위 코드에서 setTimeout으로 등록된 콜백은 현재 코드의 실행이 완료된 이후에 태스크 큐에 들어가고, 현재 마이크로태스크나 태스크 큐에 있는 다른 작업이 끝난 이후에 실행됨.