Skip to content

Instantly share code, notes, and snippets.

@kyungmi
Last active November 19, 2018 06:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyungmi/e79bc5a5417efa23e595c6307c37e81e to your computer and use it in GitHub Desktop.
Save kyungmi/e79bc5a5417efa23e595c6307c37e81e to your computer and use it in GitHub Desktop.
RxJS 퀵스타트 10장

RxJS 스케줄러 (272p)

  • https://github.com/ReactiveX/rxjs/blob/master/doc/scheduler.md
  • A Scheduler lets you define in what execution context will an Observable deliver notifications to its Observer.
    • 스케줄러는 어떤 실행 컨텍스트에서 Observable이 Observer에게 notification을 보낼지 정의할 수 있게 한다.
  • 프로그래밍 언어의 스케줄러를 효과적으로 사용할 수 있도록 만든 가상의 스케줄러. 언어의 특징을 담고 있어서 언어별로 쓰임이 다르다.

schedulers

Scheduler Description Purpose
null By not passing any scheduler, notifications are delivered synchronously and recursively. Use this for constant-time operations or tail recursive operations.
queueScheduler Schedules on a queue in the current event frame (trampoline scheduler). Use this for iteration operations.
asapScheduler Schedules on the micro task queue, which is the same queue used for promises. Basically after the current job, but before the next job. setImmediate 또는 Node.js의 process.nextTick 으로 동작한다. Use this for asynchronous conversions.
asyncScheduler Schedules work with setInterval. Use this for time-based operations.
animationFrameScheduler Schedules task that will happen just before next browser content repaint. requestAnimationFrame으로 동작한다. Can be used to create smooth browser animations.

rxjs 네임스페이스

  • 마지막 인자로 Scheduler를 지정할 수 있음
  • 지정하지 않으면 기본 스케줄러(null) 사용
from(ish: ObservableInput<T>, scheduler: Scheduler)
interval(period: number, scheduler: Scheduler)
of(values: ...T, scheduler: Scheduler)
throwError(error: any, scheduler: Scheduler)
...

rxjs.operators 네임스페이스 🧐

const { of } = rxjs;
const { tap } = rxjs.operators;
const obs$ = of('A', 'B', 'C')
.pipe(
tab(v => console.log(v, '데이터 처리 1')),
tab(v => console.log(v, '데이터 처리 2')),
tab(v => console.log(v, '데이터 처리 3')),
tab(v => console.log(v, '데이터 처리 4')),
);
console.log('subscribe 전');
obs$.subscribe(v => console.log('observer 데이터 받음: ', v));
console.log('subscribe 후');
// "subscribe 전"
// "A" "데이터 처리 1"
// "A" "데이터 처리 2"
// "A" "데이터 처리 3"
// "A" "데이터 처리 4"
// "observer 데이터 받음: " "A"
// "B" "데이터 처리 1"
// "B" "데이터 처리 2"
// "B" "데이터 처리 3"
// "B" "데이터 처리 4"
// "observer 데이터 받음: " "B"
// "C" "데이터 처리 1"
// "C" "데이터 처리 2"
// "C" "데이터 처리 3"
// "C" "데이터 처리 4"
// "observer 데이터 받음: " "C"
// "subscribe 후"
const { of, asyncScheduler } = rxjs;
const { tap, observeOn } = rxjs.operators;
const obs$ = of('A', 'B', 'C')
.pipe(
observeOn(asyncScheduler),
tap(v => console.log(v, '데이터 처리 1')),
tap(v => console.log(v, '데이터 처리 2')),
tap(v => console.log(v, '데이터 처리 3')),
tap(v => console.log(v, '데이터 처리 4')),
);
// 아래 두 경우도 동일한 동작
// const obs$ = of('A', 'B', 'C')
// .pipe(
// subscribeOn(asyncScheduler),
// tap(v => console.log(v, '데이터 처리 1')),
// tap(v => console.log(v, '데이터 처리 2')),
// tap(v => console.log(v, '데이터 처리 3')),
// tap(v => console.log(v, '데이터 처리 4')),
// );
// const obs$ = of('A', 'B', 'C', asyncScheduler)
// .pipe(
// tap(v => console.log(v, '데이터 처리 1')),
// tap(v => console.log(v, '데이터 처리 2')),
// tap(v => console.log(v, '데이터 처리 3')),
// tap(v => console.log(v, '데이터 처리 4')),
// );
console.log('subscribe 전');
obs$.subscribe(v => console.log('observer 데이터 받음: ', v));
console.log('subscribe 후');
// "subscribe 전"
// "subscribe 후"
// "A" "데이터 처리 1"
// "A" "데이터 처리 2"
// "A" "데이터 처리 3"
// "A" "데이터 처리 4"
// "observer 데이터 받음: " "A"
// "B" "데이터 처리 1"
// "B" "데이터 처리 2"
// "B" "데이터 처리 3"
// "B" "데이터 처리 4"
// "observer 데이터 받음: " "B"
// "C" "데이터 처리 1"
// "C" "데이터 처리 2"
// "C" "데이터 처리 3"
// "C" "데이터 처리 4"
// "observer 데이터 받음: " "C"
const { of, asyncScheduler } = rxjs;
const { tap, observeOn } = rxjs.operators;
const obs$ = of('A', 'B', 'C')
.pipe(
tap(v => console.log(v, '데이터 처리 1')),
tap(v => console.log(v, '데이터 처리 2')), // 여기까지 동기적으로 처리된다.
observeOn(asyncScheduler),
tap(v => console.log(v, '데이터 처리 3')),
tap(v => console.log(v, '데이터 처리 4')),
);
console.log('subscribe 전');
obs$.subscribe(v => console.log('observer 데이터 받음: ', v));
console.log('subscribe 후');
// "subscribe 전"
// "A" "데이터 처리 1"
// "A" "데이터 처리 2"
// "B" "데이터 처리 1"
// "B" "데이터 처리 2"
// "C" "데이터 처리 1"
// "C" "데이터 처리 2"
// "subscribe 후"
// "A" "데이터 처리 3"
// "A" "데이터 처리 4"
// "observer 데이터 받음: " "A"
// "B" "데이터 처리 3"
// "B" "데이터 처리 4"
// "observer 데이터 받음: " "B"
// "C" "데이터 처리 3"
// "C" "데이터 처리 4"
// "observer 데이터 받음: " "C"

애니메이션 구현하기 (281p)

  • anomationFrame 스케줄러 이용

requestAnimationFrame

  • setTimeout
    • 브라우저 상태에 영향을 받으므로 사용자가 지정한 정확한 지연 시간을 보장하지 못함
  • requestAnimationFrame
    • 지연 시간을 브라우저가 결정
    • 최소 60fps를 보장하면서 메인 스레드가 여유있는 시간에 실행됨
    • 주로 16ms 단위로 발생함
    • 이벤트 루프의 한 틱이 발생할 때마다 발생하기 때문에 부드러운 애니메이션을 만드는데 쓰임

interval(period: number, scheduler: Scheduler)

  • 첫 번째 인자로 0인 경우 두 번째 파라미터로 전달된 스케줄러에 의해 데이터 전달 주기를 결정

takeWhile(predicate: function(value, index): boolean)

  • 지정한 조건이 만족되지 않았을 때 Observable을 완료

concat(observables: ...*)

  • 완료된 Observable 뒤에 추가적인 Observable들을 연결할 수 있다.

defer(observableFactory: () => SubscribableOrPromise<T> | void)

const { defer, interval, animationFrameScheduler, concat, of } = rxjs;
const { map, takeWhile } = rxjs.operators;
const DEFAULT_DURATION = 300;
const animation = (from, to, duration = DEFAULT_DURATION) => defer(() => {
const scheduler = animationFrameScheduler;
const start = scheduler.now(); // 이 시점을 subscribe가 일어나는 시점으로 지연하기 위해 defer를 사용했다.
const interval$ = interval(0, scheduler)
.pipe(
map(() => (scheduler.now() - start) / duration),
takeWhile(rate => rate < 1),
);
return concat(interval$, of(1))
.pipe(
map(rate => from + ((to - from) * rate)),
);
});
const animation$ = animation(100, 500);
animation$.subscribe(v => console.log(v));
// 110.66666666666667
// 113.33333333333333
// 137.33333333333334
// 138.66666666666666
// 158.66666666666666
// 160
// 182.66666666666669
// 185.33333333333334
// 197.33333333333334
// 200
// 220
// 221.33333333333334
// 240
// 241.33333333333334
// 322.66666666666663
// 324
// 345.3333333333333
// 345.3333333333333
// 356
// 357.3333333333333
// 373.3333333333333
// 374.6666666666667
// 396
// 397.3333333333333
// 416
// 418.66666666666663
// 441.33333333333337
// 444
// 460
// 462.66666666666663
// 484
// 486.6666666666667
// 500
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=medium-dpi">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css">
<script src="../../node_modules/rxjs/bundles/rxjs.umd.js"></script>
<style>
.view {
overflow: hidden;
}
.view .container {
white-space: nowrap;
padding: 0px;
list-style: none;
font-size: 0;
}
.view .panel {
width: 100%;
min-height: 200px;
display: inline-block;
}
</style>
</head>
<body>
<div id="carousel" class="view">
<ul class="container">
<li class="panel" style="background-color:lightgreen">
</li>
<li class="panel" style="background-color:lightpink">
</li>
<li class="panel" style="background-color:royalblue">
</li>
<li class="panel" style="background-color:darkred">
</li>
</ul>
</div>
<script>
const THRESHOLD = 30;
const DEFAULT_DURATION = 300;
const $view = document.getElementById("carousel");
const $container = $view.querySelector(".container");
const PANEL_COUNT = $container.querySelectorAll(".panel").length;
const SUPPORT_TOUCH = "ontouchstart" in window;
const EVENTS = {
start: SUPPORT_TOUCH ? "touchstart" : "mousedown",
move: SUPPORT_TOUCH ? "touchmove" : "mousemove",
end: SUPPORT_TOUCH ? "touchend" : "mouseup"
};
const { fromEvent, merge, of, defer, animationFrameScheduler, interval, concat } = rxjs;
const {
map,
startWith,
switchMap,
takeUntil,
reduce,
share,
first,
scan,
withLatestFrom,
takeWhile
} = rxjs.operators;
function animation(from, to, duration) {
return defer(() => {
const scheduler = animationFrameScheduler;
const start = scheduler.now();
const interval$ = interval(0, scheduler)
.pipe(
map(() => (scheduler.now() - start) / duration),
takeWhile(rate => rate < 1)
);
return concat(interval$, of(1))
.pipe(
map(rate => from + (to - from) * rate)
);
});
}
function toPos(obs$) {
return obs$.pipe(
map(v => SUPPORT_TOUCH ? v.changedTouches[0].pageX : v.pageX)
);
}
function translateX(posX) {
$container.style.transform = `translate3d(${posX}px, 0, 0)`;
}
const start$ = fromEvent($view, EVENTS.start).pipe(toPos);
const move$ = fromEvent($view, EVENTS.move).pipe(toPos);
const end$ = fromEvent($view, EVENTS.end);
const size$ = fromEvent(window, "resize").pipe(
startWith(0),
map(event => $view.clientWidth)
)
const drag$ = start$.pipe(
switchMap(start => {
return move$.pipe(
map(move => move - start),
takeUntil(end$)
);
}),
share(),
map(distance => ({ distance }))
)
const drop$ = drag$.pipe(
switchMap(drag => {
return end$.pipe(
map(event => drag),
first()
)
}),
withLatestFrom(size$, (drag, size) => {
return { ...drag, size };
})
);
const carousel$ = merge(drag$, drop$)
.pipe(
scan((store, {distance, size}) => {
const updateStore = {
from: -(store.index * store.size) + distance
};
if (size === undefined) { // drag 시점
updateStore.to = updateStore.from;
} else { // drop 시점
let tobeIndex = store.index;
if (Math.abs(distance) >= THRESHOLD) {
tobeIndex = distance < 0 ?
Math.min(tobeIndex + 1, PANEL_COUNT - 1) :
Math.max(tobeIndex - 1, 0);
}
updateStore.index = tobeIndex;
updateStore.to = -(tobeIndex * size);
updateStore.size = size;
}
return { ...store, ...updateStore };
}, {
from: 0,
to: 0,
index: 0,
size: 0,
}),
switchMap(({from, to}) => from === to ?
of(to) : animation(from, to, DEFAULT_DURATION))
);
carousel$.subscribe(pos => {
console.log("캐로셀 데이터", pos);
translateX(pos);
});
</script>
</body>
</html>

이벤트 루프

eventloop

  • https://youtu.be/cCOL7MC4Pl0?t=820
  • 이벤트 루프는 반복해서 call stack과 microtask queue, task queue 사이의 작업들을 확인해 동작시킴으로써 비동기 작업을 가능하게 한다.
  • call stack이 비워져 있는 경우 microtask queue에서 작업을 꺼내 call stack에 넣는다.
  • microtask queue가 비어서 더 이상 처리할 작업이 없으면 이때 task queue를 확인해 call stack에 넣는다.

task, microtask

  • task: 비동기 작업이 순차적으로 일어나게 하지만 바로 다음 작업이 실행된다는 의미는 아님, asyncScheduler
  • microtask: 비동기 작업이 현재 실행되는 스크립트 바로 다음에 실행됨, MutationObserver, Promise, asapScheduler(Promise로 구현됨)
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise1')).then(() => console.log('promise2'));
requestAnimationFrame(() => console.log('requestAnimationFrame'));
console.log('script end');


// [chrome, firefox]
// script start
// script end
// promise1
// promise2
// requestAnimationFrame
// setTimeout

// [safari]
// script start
// script end
// promise1
// promise2
// setTimeout
// requestAnimationFrame
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment