LoginSignup
6
3

More than 1 year has passed since last update.

Native async/await

Last updated at Posted at 2021-12-24

今日はZone.jsがどうやってasync/awaitを監視することを紹介したいと思います。

Zone.jsがJavascriptの非同期の操作を監視する役割です、例えばsetTimeout, Promise.thenなど、 でも一つ監視できない非同期の操作があって、それがES2017のasync/await のことです。native async/awaitがJavascriptのPromiseを経由しなくて、nativeのPromiseあるいは直接MicrotaskQueueにスケジューリングしましたので、Javascript側で探知できないです。なので、今までAngularがずっとES2015をターゲットとしてコンパイルすることになって、async/awaitを下記のようにJavascriptのPromiseにTranspileすることになります。

function compute(a) {
  return new Promise(res => setTimeout(() => {
    res(a + 1);
  }));
}

async function testAsync() {
  let a = 0;
  console.log('before await', a);
  a = await compute(a);
  console.log('after first await', a);
}

GeneratorPromiseに変更しました。

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};

function compute(a) {
  return new Promise(res => setTimeout(() => {
    res(a + 1);
  }));
}

function testAsync() {
    return __awaiter(this, void 0, void 0, function* () {
        let a = 0;
        console.log('before await', a);
        a = yield compute(a);
        console.log('after first await', a);
    });
}

そしたら、下記のコードが正しい結果(Zone)が出力できるようになります。

async function testAsync() {
  let a = 0;
  a++;
  console.log('before await', Zone.current.name); // before await zoneA
  a = await compute(a);
  a++;
  console.log('after first await', a, Zone.current.name);  // after first await zoneA
  a = await compute(a);
  a++;
  console.log('after second await', a, Zone.current.name); // after second await zoneA
}

Zone.current.fork({name: 'zoneA'}).run(testAsync);

このソリューションがいくつディメリットがあります。

native async/await と比べて、
1. バンドルサイズが大きい
2. 性能が遅い
3. Chrome で提供されている新しいAsync Stack Traces が使えなくなります。
 
なので、Zone.jsがどうやってnative async/await をサポートすることがずっと課題になります。今年Google 内部でBuildシステムに対して、いろいろな整理とかが実施されているらしくて、AngularチームがZone.jsのasync/await問題にたいして、いろいろ解決のソリューションを探しました、今日はこれらのソリューションを簡単に説明します。

目標としては、

  1. できれば、native async/await を利用して、JavascriptのPromiseを利用しないこと。
  2. await のあとで、正しくawaitの前のZoneに戻ること。
  3. await の後で、正しくもう一回Zone.onInvoke() hookを実行すること。
  4. fakeAsync() テストを正しくサポートすること。

になります、すみません、上記の要件を細かく説明したら、すごく長くなりますので、ほかのPostで説明させていただきます。そしたら、いくつのActionを実施しました。

  1. 可能のソリューションを探します。
  2. 各ソリューションにたいして、
    • 実装難易度
    • 性能
    • Buildツールの連携

を評価すること。

ソリューション1、async/await -> Generator にTransformすること。

function compute(a) {
  return a + 1;
}

async function testAsync() {
  let a = 0;
  a++;
  console.log('before await', a, Zone.current.name);
  a = await compute(a);
  a++;
  console.log('after first await', a, Zone.current.name);
  a = await compute(a);
  a++;
  console.log('after second await', a, Zone.current.name);
}

が下記のコードになりました。

function testAsync_generator_1* () {
  let a = 0;
  a++;
  console.log('before await', a, Zone.current.name);
  a = yield compute(a);
  a++;
  console.log('after first await', a, Zone.current.name);
  a = yield compute(a);
  a++;
  console.log('after second await', a, Zone.current.name);
}

function testAsync() {
  return Zone.__awaiter(this, [], testAsync_generator_1);
}

そして、Zone.__awaiter()が下記のようになりました。

Zone.__awaiter = async function<T>(thisArg: any, _arguments: any, generator: () => Generator<T>) {
  const zone = Zone.current;
  const gen = generator.apply(thisArg, _arguments);
  let res = gen.next();
  const args = [await res.value];
  while (!res.done) {
    try {
      res = zone.run(gen.next, gen, args, 'native await');
      args[0] = await res.value;
    } catch (error) {
      zone.runGuarded(() => {
        res = gen.throw(error);
      }, undefined, [], 'native await');
    }
  }
  return args[0];
}

簡単に言えば、Javascript Promise を使わなくて、Generatoriteratorとしてロープして、await で実装する方法です。

  • メリットは:Javascript Promiseがなくなる。
  • ディメリットは:Transformのロジックが難しくなる、そして、ES2018のasync generator を対応するためにはべつのTransformを実装しないといけないです。
  • 性能:JavascriptのPromiseと大きい差がありません。
  • Debug Experience: SourceMapが有効する場合、native async/await, Javascript PromiseとこのGeneratorのソリューションが同じ

ソリューション2:await 前後beforeAwait(), afterAwait()を追加してTransformして、Zoneで新しいonBeforeAwait(), onAfterAwait() hook で実装すること。

async function testAsync() {
  console.log("async begin");
  await 1;
  console.log('after await 1');
  await asyncFunc1(); 
  console.log('after await asyncFunc1');
  await asyncFunc2();
  console.log('after await asyncFunc2');
  console.log("async end");
}

が下記のコードになりました。

async function testAsyncTranspiled() {
  let taskSignal: any;
  try {
    beforeAsync();

    taskSignal = beforeAwait();
    await 1;
    afterAwait(taskSignal);
    console.log('after await 1');


    taskSignal = beforeAwait();
    await asyncFunc1();
    afterAwait(taskSignal);
    console.log('after await asyncFunc1');

    taskSignal = beforeAwait();
    await asyncFunc2();
    afterAwait(taskSignal);
    console.log('after await asyncFunc2');

    afterAsync();
  } catch (err) {
    awaitError(err);
  } finally {
    afterAwait(taskSignal);
  }
}

になりました。そして、beforeAwait(), afterAwait() が下記のような感じです。

function beforeAsync() {
  Zone.current.onBeforeAwait();
}
function afterAsync() {
  Zone.current.onAfterAwait();
}
function beforeAwait() {
  Zone.current.onAfterAwait();
  Zone.captureZone(Zone.current);
  return Zone.current;
}
function afterAwait(taskSignal: any) {
  Zone.restoreZone(taskSignal);
  Zone.current.onBeforeAwait();
}

function awaitError(err: any) {
  Zone.current.onHandleError(err);
}

今Google内部でこのようなイメージの方法でZone.jsをES2017で対応しているらしいです。
このソリューションに対して、
- メリット:Transformの実装が簡単
- ディメリット:Zoneが新しいAPIを提供必要、一部自分のZoneSpecを実装するユーザに対して、BreakingChangesになります。
- 性能がNative と同じ
- Debug ExperienceもNativeと同じ

まとめ

今このプロジェクトが一旦終了で、Angular v14とか以降再開する予定があります、理由が:
- 今Angular CLIですでにasync/await だけJavascript PromiseにDownlevelすることを実装しましたので、開発者としては特に不便がない。
- いろいろテストして、性能的にはJavascript Promiseにも遅くない、Native async/awaitと比べて差別とても小さい。
- Debug Experienceに対して、Javascript PromiseでもSourceMapと連携して、Nativeと同じDebug Experienceを提供できる
- Bundle sizeで、Javascript Promise の__awaiterが共通のfunctionを呼び出すだけですので、ここにも問題ないです。

なので、今問題になりそうのは、Async Stack Tracesのサポートのことになります。なので、このプロジェクトの優先度が低くなって、これからZonelessの設計とともに、before/after の案ですすめる可能性が高いです。

まだ情報があったら、報告させていただきます。
どうもありがとうございました。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3