今日は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);
}
がGenerator
とPromise
に変更しました。
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
と比べて、
- バンドルサイズが大きい
- 性能が遅い
- Chrome で提供されている新しいAsync Stack Traces が使えなくなります。
なので、Zone.jsがどうやってnativeasync/await
をサポートすることがずっと課題になります。今年Google 内部でBuildシステムに対して、いろいろな整理とかが実施されているらしくて、AngularチームがZone.jsのasync/await
問題にたいして、いろいろ解決のソリューションを探しました、今日はこれらのソリューションを簡単に説明します。
目標としては、
- できれば、native
async/await
を利用して、JavascriptのPromiseを利用しないこと。 -
await
のあとで、正しくawait
の前のZoneに戻ること。 -
await
の後で、正しくもう一回Zone.onInvoke()
hookを実行すること。 -
fakeAsync()
テストを正しくサポートすること。
になります、すみません、上記の要件を細かく説明したら、すごく長くなりますので、ほかのPostで説明させていただきます。そしたら、いくつのActionを実施しました。
- 可能のソリューションを探します。
- 各ソリューションにたいして、
- 実装難易度
- 性能
- 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
を使わなくて、Generator
をiterator
としてロープして、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
の案ですすめる可能性が高いです。
まだ情報があったら、報告させていただきます。
どうもありがとうございました。