예제로 이해하는 JobQueue와 Event Loop

blackbell·2020년 7월 29일
2

javascript

목록 보기
6/6

이 글은 아래에 있는 출처의 글과 영상을 간단하게 정리한 것입니다.

다음 예제들을 살펴보도록 하겠습니다.

예제 1

Promise.resolve()
  .then(() => console.log('promise1 step1'))
  .then(() => console.log('promise1 step2'));

Promise.resolve()
  .then(() => console.log('promise2 step1'))
  .then(() => console.log('promise2 step2'));

console.log('continue');

예제 2

<script>
console.log('start');
{
  const script = document.createElement('script');
  document.body.append(script);
  script.text = `
	console.log('append script!!');
	Promise.resolve().then(() => console.log('script promise'));
  `;
}
Promise.resolve()
  .then(() => console.log('promise step1'))
  .then(() => console.log('promise step2'));
console.log('end');
</script>

예제 3

    <script>
      console.log('start');
      {
        const script = document.createElement('script');
        document.body.append(script);
        script.text = `
            console.log('append script!!3333');
            Promise.resolve()
            .then(() => console.log('script promise step1'))
            .then(() => console.log('script promise step2'));
        `;
      }
      Promise.resolve()
        .then(() => console.log('promise step1'))
        .then(() => console.log('promise step2'));
      console.log('end');
    </script>
    <script>
      Promise.resolve().then(() => console.log('script2 promise step1'));
      console.log('script2');
    </script>

예제 4

const gene = function* (max, block) {
  let i = 0;
  while (i++ < max) yield new Promise((res) => {
    block();
    res();
  });
};

const nbFor = (max, block) => {
  const iterator = gene(max, block);
  const next = ({ value, done }) => 
  	done || value.then(() => next(iterator.next()));
  next(iterator.next());
};

nbFor(10000, () => console.log('nb'));
for (let i = 0; i < 300; i++) console.log('blocking?');

예제 5

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('promiseJob1'));
  console.log('button click1');
});

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('promiseJob2'));
  console.log('button click2');
});

// button.click();

JobQueue

개별적으로 실행되는 스크립트 블럭을 하나의 잡(Job)이라고 하고 이러한 잡들(Jobs)를 적재하는 FIFO큐가 바로 잡큐(JobQueue)입니다.

<script>
// 하나의 Job
</script>
<script>
// 또 하나의 Job
</script>

Job에는 두가지 종류가 있습니다.

  • ScriptJobs – 일반적인 스크립트 코드를 실행하는 잡
  • PromiseJobs – 프라미스의 해소나 예외로 분기되어 실행될 함수가 적재되는 잡
  • At some future point in time, when there is no running execution context and the execution context stack is empty ~
  • Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts.
    // ecma262

실행중인 EC(execution context)가 없고, EC stack이 비워져 있을 때 다음 Job이 실행됨을 알 수 있습니다. 그리고 하나의 Job이 시작하게 되면 그 Job이 완전히 실행되기 전까지는 다른 Job은 시작할 수 없습니다.

Any Promise object is in one of three mutually exclusive states: fulfilled, rejected, and pending:

  • A promise p is fulfilled if p.then(f, r) will immediately enqueue a Job to call the function f.
  • A promise p is rejected if p.then(f, r) will immediately enqueue a Job to call the function r.
  • A promise is pending if it is neither fulfilled nor rejected.
    // ecma262

그리고 Promise의 경우 then 이후가 PromiseJobs로 즉시 등록되는 것을 알 수 있습니다.

Promise.resolve().then(() => console.log('promiseJob1'));
console.log('continue');

// continue
// promiseJob1

EC stack이 다 비워진 후에 PromiseJob이 실행되는 것을 알 수 있습니다.

예제 1

Promise.resolve()
  .then(() => console.log('promise1 step1'))
  .then(() => console.log('promise1 step2'));

Promise.resolve()
  .then(() => console.log('promise2 step1'))
  .then(() => console.log('promise2 step2'));

console.log('continue');
// 실행결과
// continue
// promise1 step1
// promise2 step1
// promise1 step2
// promise2 step2
  1. ScriptJobs에 해당 스크립트 내용이 들어간 후에
  2. 바로 실행됩니다.
  3. step1들이 차례로 PromiseJobs로 잡큐에 등록됩니다.
  4. continue가 console에 찍힌 후 EC stack이 비어지게 됩니다.
  5. 따라서 PromiseJobs인 promise1 step1이 해소되면서
  6. promise1 step2가 PromiseJobs로 다시 잡큐에 등록됩니다.
  7. 이와 같이 차례로 해소되게 되면 위의 결과가 나옵니다.

예제 2

<script>
console.log('start');
{
  const script = document.createElement('script');
  document.body.append(script);
  script.text = `
	console.log('append script!!');
	Promise.resolve().then(() => console.log('script promise'));
  `;
}
Promise.resolve()
  .then(() => console.log('promise step1'))
  .then(() => console.log('promise step2'));
console.log('end');
</script>
  1. ScriptJobs로 해당 script내용이 잡큐에 등록된 후에
  2. script내용이 실행되면서 다시 ScriptJobs를 생성하여 추가합니다.

여기서 생성된 script태그가 ScriptJobs가 되어 잡큐에 등록된다고 생각하여 아래와 같은 결과를 예측할 수 있습니다.

// start
// end
// promise step1
// promise step2
// append script!!
// script promise

하지만, script태그를 생성하든 innerHTML로 넣어주던 자바스크립트의 DOM제어가 또 다른 스크립트를 만들어내는 경우, 새로운 잡으로 등록되지 않고 현재 잡에서 연속적으로 실행되게 됩니다. 따라서 아래와 같은 결과가 나옵니다.

// 실행결과
// start
// append script!!
// end
// script promise
// promise step1
// promise step2

예제 3

    <script>
      console.log('start');
      {
        const script = document.createElement('script');
        document.body.append(script);
        script.text = `
            console.log('append script!!3333');
            Promise.resolve()
            .then(() => console.log('script promise step1'))
            .then(() => console.log('script promise step2'));
        `;
      }
      Promise.resolve()
        .then(() => console.log('promise step1'))
        .then(() => console.log('promise step2'));
      console.log('end');
    </script>
    <script>
      Promise.resolve().then(() => console.log('script2 promise step1'));
      console.log('script2');
    </script>

그렇다면 이제 예제3은 쉽게(?) 결과를 알 수 있습니다.

start
append script!!3333
end
script promise step1
promise step1
script promise step2
promise step2
script2
script2 promise step1

첫번째 script는 ScriptJobs로 잡큐에 등록되어 예제 2처럼 실행되고,
두번째 script도 마찬가지로 실행이 됩니다.

예제 4

const gene = function* (max, block) {
  let i = 0;
  while (i++ < max) yield new Promise((res) => {
    block();
    res();
  });
};

const nbFor = (max, block) => {
  const iterator = gene(max, block);
  const next = ({ value, done }) => 
  	done || value.then(() => next(iterator.next()));
  next(iterator.next());
};

nbFor(10000, () => console.log('nb'));
for (let i = 0; i < 300; i++) console.log('blocking?');

gene은 계속 Promise를 반환하고 nbFor함수의 next는 그 Promise를 해소한 뒤에 다음 Promise를 받아옵니다. 이와 같은 반복작업이 이루어지게 됩니다.
다음 코드를 실행시키게 되면 'blocking'이 300번 console에 출력된 후에 'nb'가 10000번 출력되게 됩니다.
block이 되지 않고 'nb'들이 출력되지만, 브라우저가 먹통이 되고 다음 프레임으로 넘어가지 않는 현상이 발생합니다.

예제 5

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('promiseJob1'));
  console.log('button click1');
});

button.addEventListener('click', () => {
  Promise.resolve().then(() => console.log('promiseJob2'));
  console.log('button click2');
});

// button.click();

버튼을 click하게 되면 아래와 같은 결과가 나옵니다.

// button click1
// promiseJob1
// button click2
// promiseJob2

하지만, button.click();으로 click이벤트를 발생시킬 경우 EC stack이 비워지지 않으면서 PromiseJob은 나중에 해소되게 됩니다.

// button click1
// button click2
// promiseJob1
// promiseJob2

이제 EventLoop에 대해 간단히 알아보도록 하겠습니다.
1. js, css 파싱 후 DOM, CSSOM 만듬
2. DOM과 CSSOM을 이용하여 Render Tree 생성 ( Recalc Style )
3. Layout → 좌표계산
4. Update layer Tree
5. Paint → paint 순서 결정 ( paint record )
6. Composite(합성) 합성은 웹 페이지의 각 부분을 레이어로 분리해 별도로 레스터화하고 컴포지터 스레드(compositor thread)라고 하는 별도의 스레드에서 웹 페이지로 합성하는 기술

위의 과정이 필요에 따라 매프레임마다 일어나게 됩니다.
rAF(requestAnimationFrame)은 스타일 계산이 일어나기 전에 주기적으로 js를 실행할 수 있게 해줍니다.


출처 : https://www.youtube.com/watch?v=cCOL7MC4Pl0

예시1

https://repl.it/@kyujong93/box-transform#script.js

btn.addEventListener('click', () => {
  box.style.transform = 'translateX(500px)';
  box.style.transition = 'transform 0.5s ease-in-out';
  box.style.transform = 'translateX(250px)'; 
})

우리가 기대하는 것은 box가 500px이동했다가 250px로 서서히 이동하는 모습을 보고 싶습니다. 하지만 위의 코드를 250px위치에 가만히 있습니다. 위의 eventLoop 동작과정을 통해 이해해보면 위의 코드는 한 프레임에 실행되게 되고 마지막 코드가 반영이 된 것임을 알 수 있습니다.

우리가 원하는 모습으로 실행시키기 위해서는 어떻게 해야될까요?☝️

btn.addEventListener('click', () => {
  box.style.transform = 'translateX(500px)';
  requestAnimationFrame(() => {
    requestAnimationFrame(() => { 
      box.style.transform = 'translateX(250px)'; 
      box.style.transition = 'transform 0.5s ease-in-out';
    })
  })
})

requestAnimationFrame을 통해 다음 프레임에 실행되도록 해주면 됩니다.
그렇게 되면 500px이동한 후 렌더링이 되고 다음 프레임에 250px위치로 서서히 이동하게 될 것입니다.

btn.addEventListener('click', () => {
  box.style.transform = 'translateX(500px)';
  requestAnimationFrame(() => {
    box.style.transform = 'translateX(250px)'; 
    box.style.transition = 'transform 0.5s ease-in-out';
  })
})

다음과 같은 코드를 실행하면 위의 코드와 다르게 초기 위치에서 250px로 서서히 이동함을 볼 수 있습니다. 하지만 이는 현재 20.07.29 chrome, firefox에서는 예상대로 동작하지만 safari에서는 위에 동작했던 것처럼 동작합니다.
이것은 safari는 스타일계산전에 rAF가 발생하는 chrome, firefox와는 달리 safari는 렌더링후에 rAF가 발생하기 때문입니다.
(https://www.youtube.com/watch?v=cCOL7MC4Pl0 의 영상이 2018년인데 그 당시에 firefox는 rAF의 순서가 렌더링 뒤쪽이었지만 현재는 스타일 계산전에 실행됨을 알 수 있습니다 👏 하지만, safari는 그대로인 거 같습니다. edge, IE 등은 테스트해보지 않아 모르겠습니다.)

예시2

https://repl.it/@kyujong93/while-true#index.html

(위의 우주복을 입은 캐릭터는 gif, 아래의 파란 박스는 animation으로 움직이고 있는 모습입니다.)

위의 예시는 click버튼을 누르게 되면 while (true); 라는 코드가 실행됩니다.
따라서 eventLoop에 따라 렌더링이 되지 않아 gif와 box는 움직이지 않을 것으로 예상되지만 box는 compositor thread에 의해서 계속 움직입니다. (transform 속성을 주게 되면 1개의 layer가 추가로 만들어집니다.)

잘 모르겠는 것...

하지만 위의 예시2도 브라우저에 따라 다르게 보였습니다😭
firefox와 safari에서는 우리가 예상했던대로 while (true);로 인해 브라우저가 먹통이 되면서 렌더링이 되지 않아 gif는 멈추게 되고, box는 계속 움직입니다. 하지만 chrome에서는 gif, box 모두 움직입니다. 이 부분에 대해서는 추가적으로 더 찾아봐야할 것 같습니다...

출처

https://www.bsidesoft.com/5385
https://tc39.es/ecma262/#sec-jobs-and-job-queues
https://www.w3.org/TR/html51/webappapis.html#integration-with-the-javascript-job-queue
https://html.spec.whatwg.org/multipage/webappapis.html#step1
https://www.youtube.com/watch?v=cCOL7MC4Pl0
https://d2.naver.com/helloworld/5237120

profile
알고 싶은게 많은 프론트엔드 개발자입니다.

0개의 댓글