概要
Angularで出てきたfakeAsyncやwhenStableを使う時に、microtasksの話が出たのでちゃんと理解しようと調べてみました。
実験
以下のjsのログ順はどうなるでしょうか?
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');
結果
このようになります。
script start script end promise1 promise2 setTimeout
なぜこうなるのか?
これはevent loopがtasksやmicrotasksをどう処理するかを理解している必要があります。
大別すると2つあり、
タスクキュー | 例 |
---|---|
tasks | 実行スクリプト、setTimeoutなど |
microtasks | promiseのcallback、mutation observerのcallbackなど |
そして実行順は
タスクキュー | 例 |
---|---|
tasks | FIFOの順。間にブラウザの描画が挟まる |
microtasks | 1tasksが終了する毎にキューに詰まっている全microtasksを実行。FIFO |
となっています。これを元に先程のスクリプトを説明すると、
- 実行スクリプト開始
scritp start
をログに- setTimeoutをTasksキューに詰める
- promiseをMicrotasksキューに詰める
scritp end
をログに- Tasksが一旦完了したので、Microtasksを実行していく
promise1
をログにpromise2
のpromiseをMicrotasksキューに詰めるpromise2
をログに- Microtasksが完了したので、次のTasksへ
setTimeout
をログに
Observableはどうなるのか?
先程のコードにobservableを入れてみました。
console.log('script start'); const obs = Rx.Observable.create( observer => { console.log('observer1'); const now = Date.now(); observer.next(now); } ); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); obs.subscribe( v => { console.log("1st: " + v); } ); console.log('script end');
結果
このようになります。
script start observer1 1st: 1489366974287 script end promise1 promise2 setTimeout
実行スクリプトの中で処理されている事が分かります。
なぜこうなるのか?
RxJS v4 defaulted to a scheduler called Rx.Scheduler.asap which schedules on the micro task queue. RxJS v5 however defaults to having no scheduler at all; v4 called this Rx.Scheduler.immediate. This was done to increase performance for the most common use cases.
ref: rxjs/MIGRATION.md at master · ReactiveX/rxjs · GitHub
公式で説明されているのですが、以前はmicrotasksに詰め込んでいたのですが、v5からは実行スクリプトの中で処理するロジックに変わったようです。
ただし中身が非同期であれば
今度はobservableの中身にsetTimeoutを入れてみます。
console.log('script start'); const obs = Rx.Observable.create( observer => { console.log('observer1'); setTimeout(function() { console.log('observer setTimeout'); const now = Date.now(); observer.next(now); observer.complete(); }, 0); } ); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); obs.subscribe( v => { console.log("1st: " + v); } ); console.log('script end');
結果
script start observer1 script end promise1 promise2 setTimeout observer setTimeout 1st: 1490341422365
先程の例で実行スクリプトで即時処理されている(observer1
の位置)ことがわかりますが、その中でsetTimeoutなどの非同期Taskを実行すればそれはTaskキューの最後につめられるので、実行も最後になり、subscribe
の処理も最後に回されます。
fakeAsyncやasyncのwhenStable
公式のTestドキュメントには、非同期のテストについて以下のように書かれています。
以下のようなコンポーネントのngOnInitで、Promise callbackが呼ばれている場合、
@Component({ selector: 'twain-quote', template: '<p class="twain"><i>{{quote}}</i></p>' }) export class TwainComponent implements OnInit { intervalId: number; quote = '...'; constructor(private twainService: TwainService) { } ngOnInit(): void { this.twainService.getQuote().then(quote => this.quote = quote); } }
async whenStable
it('should show quote after getQuote promise (async)', async(() => { fixture.detectChanges(); fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }); }));
fakeAsync
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => { fixture.detectChanges(); tick(); // wait for async getQuote fixture.detectChanges(); // update view with quote expect(el.textContent).toBe(testQuote); }));
このようにwhenStable
やtick
によって強制的にmicrotasksを処理してからexpectするように説明しています。
これは先程のtasksとmicrotasksの順番が関係しているためです。
逆に先ほどObservableはmicrotasksに入らないと説明したように、元のコンポーネントをPromiseでなくObservable かつ 中身がmacrotaskの処理でない場合、このようなfakeAsyncは無くてもちゃんとテストは成功しました。
まとめ
イベントループについて理解すると、Angularのテストが書きやすくなると思います。