2019-03-04-19 이벤트루프와 JS동작원리
JS동작원리
- heap : 객체들은 힙영역에 적재된다.
- call stack : 스택프레임이 있는곳이고, 여기서 코드가 실행이 된다.
- webapi : JS엔진에서는 제공되지 않는 API들이다. 웹브라우저에 빌트인. js의 한부분이라기보다 js코어 윗부분에 내장되어있다.
function main() {
console.log('A');
setTimeout(
function display() { console.log('B'); }, 0);
console.log('C');
}
main();
// result
A
C
B
- setTimeout(function() { doing something}, 0)은 JS엔진에 0초뒤에 무언가를 하라가 아니라 적어도 0초뒤에 시작하라는 의미에 가깝다.
- main() 함수를 호출을 하면, stack프레임에 push가 된다. 브라우저에서는 main함수에 있는 첫번째 상태?를 스택에 푸쉬한다.(console.log(‘A’)); 이 상태는 바로 실행이 되고 실행이 끝나면 pop된다. 디스플레이는 A가 출력이 된다.
- main함수안에 있는 다음 선언문(setTimeout)이 콜스택에 푸쉬하고 실행한다. setTimeout함수는 브라우저api를 지연시키는 콜백함수다.
- 브라우저에서 exec()함수가 돌고있는 동안 스택에 console.log(‘C’)가 푸쉬된다. 이 경우 0ms으로 콜백을 설정했기때문에 callback은 메세지큐에 추가 된다.
- 마지막 선언문이 실행되고 main함수프레임은 콜스택에서 pop된다.
- 콜백 exec()가 콜스택으로 푸쉬되고 실행이 된다.
Non-Blocking I/O
- JS 대부분의 I/O는 논블로킹이다.(http request, database operations, disk reads and write)…
- 싱글스레드는 런타임에 작업을 수행핟로고 요청하고 콜백함수를 제공하고 다른함수를 제공한다.
스택프레임
- 우리가 함수를 호출하면 스택프레임이 생성이 된다. 스택프레임은 함수안에서 선언된 변수를 담고 있다. 함수가 종료되면 스택프레임이 제거된다.
메세지큐
- JS는 메세지큐를 가지고 있다. 각각의 메세지는 단일함수를 가지고 있다. 만약에 스택이 비어있으면, JS엔진은 메세지큐가 비어있는지 확인한다. 비어있지 않으면 엔진은 첫번째 메세지(함수)를 제거하고 실행한다.첫번째 메세지가 실행되면 스택프레임이 생성이 되고 스택에 쌓인다. 함수가 다 실행이 되면 스택에서 pop된다. 그리고 다시 메세지큐가 비었는지 확인한다.
- 메세지들은 어떻게 메세지큐에서 끝날까? 일반적인 방법으로는 이벤트를 추가하는것이다(DOM elements, XMLHttpRequest, server-sent events, etc), setTimeout, setInterval.
- setTimeout()을 호출하면, 브라우저나 node.js가 타이머를 시작한다. 타이머가 만료되면 콜백함수는 메세지큐에 push된다. 이벤트루프는 콜스택에 우선순위를 준다. 그리고 콜스택에 아무것도 없으면 메세지큐로 가지서 이벤트?를 콜스택으로 올리고 실행을 한다.
- 비동기로 호출되는 함수들은 callstack에 쌓이지 않고 Task queue에 enqueue된다.
이벤트큐 이벤트테이블 이벤트루프
- 우리가 비동기함수를 호출을 하면 그것들이 이벤트테이블에 추가가 된다.
- 이벤트테이블은 함수를 실행하지도 않고 콜스택에 추가하지도 않는다. 이벤트를 추적하여 이벤트큐에 보내는데 목적이 있다.
- 이벤트큐는 함수호출을 이벤트테이블로부터 전달받는다. 여기로 이벤트루프가 들어온다.
- 이벤트루프는 콜스택이 비어있는지 수시로 체크한다. 만약에 콜스택이 비어있으면 이벤트큐를 확인한다. 이벤트큐에 그 이벤트?를 콜스택으로 옮긴다.
setTimeout(() => console.log('first'), 0)
console.log('second')
- setTimeout은 이벤트테이블에 추가되고, 이벤트테이블에서 이벤트큐로 전달하고 이벤트큐에서 이벤트루프가 선택하고 실행할때까지 기다리린다.
setTimeout(() => recursion(), 0);
- 재귀를 호출하지 말고 setTimeout을 통해 재귀를 호출하면 스택오버플로우를 피할 수 있다.
JS Runtime
- JS엔진은 heap, callstack으로 구분이 된다. 힙은 모든 메모리할당이 이루어지는 곳이고, 콜스택은 우리가 프로그램에 있는 위치를 기록하는 자료구조이다. 프로그램에 실행컨텍스트가 있으면 스택에 실행컨텍스트를 푸쉬팝한다?
동시성모델과 이벤트루프
- JS는 싱글쓰레드 기반으로 동작한다. 이는 한번에 한가지 일만 하는 의미이다. 싱글쓰레드는 동시성이슈가 발생하지 않도록 하는 이점이 있다.
- 이와 동시에 코드를 작성할때, 쓰레드를 차단시키는 것에 집중을 해야한다. 동기화네트워크호출이나 무한루프와 같은.
- 이벤트루프는 콜스택에 실행해야되는 함수가 있는지 계속 체크를 한다.
ES6 Job Queue
- ES6에서는 Promise를 사용하기 위해 Job Queue가 등장한다. 콜스택에 넣기보다, 비동기함수의 결과를 실행하기 위한 방법이다.
- 이벤트루프큐의 제일위의 있는 층이다.
비동기
- 비동기란? 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 JS의 특성이다.
- ajax가 비동기의 가장 흔한 사례였다.
function getData() {
var tableData;
$.get('https://domain.com/products/1', function (response) {
tableData = response;
});
return tableData;
}
console.log(getData()); // undefined
- 왜 콘솔의 결과는 undefined일까??
- ajax가 비동기이기때문에 .get()의 로직을 기다려주지 않고 바로 실행을 했기때문이다.
- 비동기의 두번째사례는 setTimeout이다. setTimeout은 WebAPI의 한종류다.
// #1
console.log('Hello');
// #2
setTimeout(function () {
console.log('Bye');
}, 3000);
// #3
console.log('Hello Again');
JsCopy
- 위에서 비동기처리 방식의 문제점은 콜백을 통해 해결을 할 수 있다.
function getData(callbackFunc) {
$.get('https://domain.com/products/1', function (response) {
callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
});
}
getData(function (tableData) {
console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});
- 하지만 콜백이 깊어지면 콜백지옥이 생긴다. 콜백지옥을 해결하기 위해 Promise나 Async를 사용한다.
Promise
- 비동기 조작의 최종완료나 실패를 표현하는 객체이다.
- promise는 함수에 콜백을 전달하는 대신, 콜백을 첨부하는 방식이다.
- promise는 Pending(대기), Fulfilled(이행), Rejected(실패) 3가지 상태가 있다.
- Pending : 비동기 처리 로직이 아직 완료되지 않은 상태
- Fulfilled : 비동기 처리가 완료되어 프로미스가 결과값을 반환해준 상태
- Rejected : 비동기 처리가 실패하거나 오류가 발생한 상태
new Promise(function (resolve, reject) {
// ...
});
- 함수인자 resolve를 실행하면 Fulfilled상태가 된다. ``` function successCallback(result) { console.log(‘Audio file ready at URL: ‘ + result); }
function failureCallback(error) { console.log(‘Error generating audio file : ‘ + error); }
createAudioFileAsync(audioSettings, successCallback, failureCallback);
- 위의 형태로 콜백을 전달받기 보다 Promise객체를 반환해주면 간단하게 작용할 수 있다.
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);
// more simple const promise = createAudioFileAsync(audioSettings); promise.then(successCallback, failureCallback);
#### 프로미스 실행순서
//quiz1 setTimeout(function() { // (A) console.log(‘A’); }, 0); Promise.resolve().then(function() { // (B) console.log(‘B’); }).then(function() { // (C) console.log(‘C’); });
//quiz2 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’);
- promise는 마이크로 태스크를 쓴다. 마이크로 태스크는 일반 태스크보다 우선순위가 높은 태스크라고 할 수 있다.
- 태스크큐에 대기중인 태스크가 있더라도 마이크로 태스크가 먼저 실행이 된다.
- setTimeout()은 콜백A를 태스크큐에 추가를 하고, 프로미스의 then()메소드는 콜백B를 태스크큐가 아닌 마이크로 태스크큐에 추가를 한다.
- callStack이 비었는지 이벤트루프가 확인을 하면, 마이크로태스크큐에 있는 콜백B를 실행을 한다. 그러고 두번쨰 then()메소드가 콜백C를 마이크로 테스크큐에 추가를 한다. 다시 이벤트루프가 마이크로테스크큐에서 콜백C를 실행하고 이후에 비어있으면 콜백A를 실행한다.
### Promise Guarantees
- 콜백함수를 전달해주는 고전방식과 달리 Promise는 밑의 특징들이 보장이 된다.
1. 콜백은 JS 이벤트루프가 현재 실행중인 콜스택을 완료하기 이전까지 절대 호출되지 않는다.
2. 비동기 작업이 성공하거나 실패한 뒤에 then()을 이용하여 추가한 콜백의 경우에도 해당한다.
3. then()을 여러번 사용해서 여러개의 콜백을 추가할 수 있다.
- promise의 장점은 chaining이다.
### Promise chaining
- 여러개의 비동기작업을 순차적으로 처리할 때 사용한다.
- then()은 새로운 Promise를 반환한다. 처음 promise와는 다른 promise이다.
const promise = doSomething(); const promise2 = promise.then(successCallback, failureCallback);
// or
const promise2 = doSomething().then(successCallback, failureCallback);
//콜백지옥 doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log(‘Got the final result: ‘ + finalResult); }, failureCallback); }, failureCallback); }, failureCallback);
// Promise사용
doSomething().then(function(result) { return doSomethingElse(result); }) .then(function(newResult) { return doThirdThing(newResult); }) .then(function(finalResult) { console.log(‘Got the final result : ‘ + finalResult); }) .catch(failureCallback);
// refectoring
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(Got the final result: ${finalResult}
);
})
.catch(failureCallback);
## setTimeout
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], …)```
- function or string of code to execute.