41장. 타이머
41-1. 호출 스케줄링
- 일정 시간이 경과된 이후에 호출되도록 함수 호출을 예약하려면 타이머 함수를 사용한다.
- 이를 호출 스케줄링(scheduling a call) 이라 한다.
- 타이머 함수는 ECMAScript 사양에 정의된 빌트인 함수 아니다.
- 브라우저 환경과 Node.js 환경에서 모두 전역 객체의 메서드로서 타이머 함수를 제공한다.
- JS 엔진은 단 하나의 실행 컨텍스트 스택을 갖기 때문에 두 가지 이상의 태스크 동시 실행할 수 없다.
- 싱글 스레드로 동작한다.
- 이런 이유로 타이머함수는 비동기 처리방식으로 동작한다.
41-2. 타이머 함수
- delay ms 단위
- delay default: 0
- 타이머가 만료되었다고 콜백 함수 즉시 호출을 보장하진 않는다.
- 태스크 큐에 콜백 함수를 등록하는 시간을 지연할 뿐
- delay 4ms 이하인 경우 최소 지연 시간 4ms 지정됨.
// setTimeout
const timerId = setTimeout(name => console.log(`Hi! ${name}.`), 100, 'Lee');
// return 값 브라우저환경: 숫자, Node.js 환경: 객체
// clearTimeout 함수의 인수로 전달하여 타이머 취소.
// 타이머가 취소되면 setTimeout 함수의 콜백 실행되지 않음.
cleartTimeout(timerId);
// setInterval
const count = 1;
const timerId = setInterval(() => {
console.log(count);
if (count++ === 5) clearInterval(timerId);
}, 1000);
41-3. 디바운스와 스로틀
- scroll, resize, input, mousemove 와 같은 이벤트는 짧은 시간 간격으로 연속해서 발생하는데 여기 바인딩 된 이벤트 핸들러는 과도하게 호출되어 성능에 문제를 일으킬 수 있다.
- 디바운스와 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화해 과도한 이벤트 핸들러 호출을 방지하는 기법이다.
디바운스
- 짧은 시간 간격으로 발생하는 이벤트를 그룹화해서 마지막에 한 번만 이벤트 핸들러가 호출되도록 한다.
스로틀
- 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 한다.
<!-- example1 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button class="button">click me</button>
<pre>일반 클릭 이벤트 카운터 <span class="normal-msg">0</span></pre>
<pre>debounce 클릭 이벤트 카운터 <span class="debounce-msg">0</span></pre>
<pre>throttle 클릭 이벤트 카운터 <span class="throttle-msg">0</span></pre>
<script>
const $button = document.querySelector('.button');
const $normalMsg = document.querySelector('.normal-msg');
const $debounceMsg = document.querySelector('.debounce-msg');
const $throttleMsg = document.querySelector('.throttle-msg');
const debounce = (callback, delay) => {
let timerId;
return event => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(callback, delay, event);
}
};
const throttle = (callback, delay) => {
let timerId;
return event => {
if (timerId) return;
timerId = setTimeout(() => {
callback(event);
timerId = null;
}, delay, event);
};
};
$button.addEventListener('click', () => {
$normalMsg.textContent++;
});
$button.addEventListener('click', debounce(() => {
$debounceMsg.textContent++;
}, 500));
$button.addEventListener('click', throttle(() => {
$throttleMsg.textContent++;
}, 500));
</script>
</body>
</html>
<!-- example2 -->
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.container {
width: 300px;
height: 300px;
background-color: rebeccapurple;
overflow: scroll;
}
.content {
width: 300px;
height: 1000vh;
}
</style>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="container">
<div class="content"></div>
</div>
<div>
일반 이벤트 핸들러 scroll 처리 횟수
<span class="normal-count">0</span>
</div>
<div>
스로틀 이벤트 핸들러 scroll 처리 횟수
<span class="throttle-count">0</span>
</div>
<input type="text">
<div class="msg"></div>
<script>
const $container = document.querySelector('.container');
const $normalCount = document.querySelector('.normal-count');
const $throttleCount = document.querySelector('.throttle-count');
const throttle = (callback, delay) => {
let timerId;
return event => {
if (timerId) return;
timerId = setTimeout(() => {
callback(event);
timerId = null;
}, delay, event);
};
};
let normalCount = 0;
$container.addEventListener('scroll', () => {
$normalCount.textContent = ++normalCount;
});
let throttleCount = 0;
$container.addEventListener('scroll', throttle(() => {
$throttleCount.textContent = ++throttleCount;
}, 100));
//
const $input = document.querySelector('input');
const $msg = document.querySelector('.msg');
const debounce = (callback, delay) => {
let timerId;
return event => {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(callback, delay, event);
}
};
$input.oninput = debounce(e => {
$msg.textContent = e.target.value;
}, 300);
</script>
</body>
</html>