Carpe Diem

備忘録

Tasks(Macrotasks), Microtasksについて

概要

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');

MicroTask1

結果

このようになります。

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

となっています。これを元に先程のスクリプトを説明すると、

  1. 実行スクリプト開始
  2. scritp startをログに
  3. setTimeoutをTasksキューに詰める
  4. promiseをMicrotasksキューに詰める
  5. scritp endをログに
  6. Tasksが一旦完了したので、Microtasksを実行していく
  7. promise1をログに
  8. promise2のpromiseをMicrotasksキューに詰める
  9. promise2をログに
  10. Microtasksが完了したので、次のTasksへ
  11. 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');

MicroTask2

結果

このようになります。

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');

MicroTask3

結果

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);
  }
}

Testing - ts - GUIDE

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);
  }));

このようにwhenStabletickによって強制的にmicrotasksを処理してからexpectするように説明しています。
これは先程のtasksとmicrotasksの順番が関係しているためです。

逆に先ほどObservableはmicrotasksに入らないと説明したように、元のコンポーネントをPromiseでなくObservable かつ 中身がmacrotaskの処理でない場合、このようなfakeAsyncは無くてもちゃんとテストは成功しました。

まとめ

イベントループについて理解すると、Angularのテストが書きやすくなると思います。

ソース