Event loop
- 기본적으로 JS는 싱글 쓰레드로 동작한다, 그러나 비동기 작업이 가능하게 해주는 것들은 다음과 같다
- event loop
- task queue
- micro task queue
- 브라우저/Node.js API
- JS 런타임 엔진 외부에서 비동기 실행을 돕기 위해 만들어진 장치
Call stack과 event loop
- 호출 스택(call stack)은 JS가 수행해야 할 코드나 함수를 순차적으로 담아두는 스택이다
- 이벤트 루프의 역할
- ‘call stack이 비어 있는지 확인’
- ‘코드를 실행’
- call stack에 실행해야 할 이벤트가 있다면 JS 엔진으로 실행한다
- 위 2가지 작업이 싱글 쓰레드에서 순차적으로 일어나며 동시에 일어날 수 없다
function bar() {
console.log('bar')
}
function baz() {
console.log('baz')
}
function foo() {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
- foo()가 호출 스택에 먼저 들어간다.
- foo() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
- 2의 실행이 완료된 이후 다음 코드로 넘어간다. (아직 foo()는 존재)
- setTimeout(bar(), 0)이 호출 스택에 들어간다.
- 4번에 대해 타이머 이벤트가 실행되어 태스크 큐로 들어가고, 그 대신 바로 스택에서 제거된다.
- baz()가 호출 스택에 들어간다.
- baz() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
- 7의 실행이 완료된 이후 다음 코드로 넘어간다.
- 더 이상 baz()에 남은 것이 없으므로 호출 스택에서 제거된다. (아직 foo()는 존재)
- 더 이상 foo()에 남은 것이 없으므로 호출 스택에서 제거된다.
- 이제 호출 스택이 완전히 비워졌다.
- 이벤트 루프가 호출 스택이 비워져 있다는 것을 확인했다. 그리고 태스크 큐를 확인하니 4번에 들어갔던 내용이 있어 bar()를 호출 스택에 들어보낸다.
- bar() 내부에 console.log가 존재하므로 호출 스택에 들어간다.
- 13의 실행이 끝나고 다음 코드로 넘어간다. (아직 bar() 존재)
- 더 이상 bar()에 남은 것이 없으므로 호출 스택에서 제거된다.
Task queue
- 실행해야 할 태스크의 집합
- 이벤트 루프는 Task queue를 1개 이상 가지고 있다
- 참고로 queue란 이름과 달리 자료구조는 queue 형태가 아니라 set 형태를 띠고 있다
- 이유는 선택된 queue 중에 실행 가능한 가장 오래된 태스크를 가져와야 하기 때문이다
- 실행 해야할 태스크란 비동기 함수의 콜백 함수나 이벤트 핸들러를 의미
- 비동기 함수같은 것들은 누가 처리하나?
- 메인 쓰레드가 아닌 별도의 쓰레드에서 수행되며 Task queue에 작업을 할당하고 처리하는 것은 브라우저나 Node.js의 역할
- 만약 이런 작업들이 메인 쓰레드에서 이뤄진다면 JS는 비동기 작업이 불가할 것이다, 메인이 아닌 별도의 쓰레드가 작업하기에 비동기 작업이 가능하다
Micro Task queue
- 이벤트 루프는 1개의 Micro Task queue를 갖고 있다
- 대표적으로 Promise가 있으며 Task queue보다 우선권을 갖는다
- 각 태스크의 들어가는 대표적인 작업
- Task queue: setTimeout, setInterval, setImmediate
- Micro Task queue: process, nextTick, Promises, queueMicroTask, MutationObserver
function foo() {
console.log('foo');
}
function bar() {
console.log('bar');
}
function baz() {
console.log('baz');
}
setTimeout(foo, 0);
Promise.resolve().then(bar).then(baz);
- 그렇다면 렌더링은 언제 실행될까?
- 태스크 큐를 실행하기 앞서 마이크로 태스크 큐를 실행한 뒤에 렌더링이 일어난다
<html>
<body>
<ul>
<li>동기 코드: <button id="sync">0</button></li>
<li>태스크: <button id="macrotask">0</button></li>
<li>마이크로 태스크: <button id="microtask">0</button></li>
</ul>
<button id="macro_micro">모두 동시 실행</button>
<script>
const sync = document.getElementById('sync');
const macrotask = document.getElementById('macrotask');
const microtask = document.getElementById('microtask');
const macro_micro = document.getElementById('macro_micro');
sync.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i;
}
});
macrotask.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
setTimeout(() => {
macrotask.innerHTML = i;
}, 0);
}
});
microtask.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
queueMicrotask(() => {
microtask.innerHTML = i;
});
}
});
macro_micro.addEventListener('click', function () {
for (let i = 0; i <= 100000; i++) {
sync.innerHTML = i;
}
setTimeout(() => {
macrotask.innerHTML = i;
}, 0);
queueMicrotask(() => {
microtask.innerHTML = i;
});
});
</script>
</body>
</html>
- 동기 코드는 100,000이라는 숫자가 한 번에 나타남
(숫자가 올라가는 게 보이지 않고 한번에 100,000이 보임)
- 태스크 큐는 모든 setTimeout 콜백이 큐에 들어가기 전까지 잠깐의 대기 시간을 갖다가 1~100,000까지 순차적으로 렌더링
(숫자가 올라가는 게 보임)
- 마이크로 태스크 큐는 동기 코드와 동일
- 이해를 돕는 자료