async/await
の話も入れてほしいというご要望があったので、続きを書きました。
前回記事:今日までコールバックと戦ってきたみんなを、Promiseを信じた魔法少女を、私は泣かせたくない。
(※なぜか『魔法少女まどかマギカ』の一部ネタバレが含まれるので注意してください)
人物紹介 (適当)
- まどか
-
Promise
で戦う魔法少女。歴代コールバック対策の全てを無かったことにしようと目論む。
-
- ほむほむ
- 12年間ずっとまどかに
Promise
の使い方を教えてきた謎の魔法少女。
- 12年間ずっとまどかに
- マミさん
- 目新しい技術を手にすると何も恐くなくなって死ぬ。
- さやかちゃん(笑)
- コールバックや
Promise
が嫌いだったが、async/await
で魔法少女になった。
- コールバックや
- 杏子
- クロージャの
this
について調べている途中で頭がおかしくなった父親を持つ、心優しい魔法少女。
- クロージャの
アンタが噂のasync/awaitってやつか。妙な技を使いやがる。
まずはPromise
とasync/await
で順次実行を比較。
const results = {};
Promise.resolve()
.then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge)
.then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
.then(() => console.log(results.hoge + results.piyo));
(async() => {
const hoge = await $.get('hoge.txt');
const piyo = await $.get('piyo.txt');
console.log(hoge + piyo);
})();
構成は似ていますが、async/await
側は劇的にコード量が減っています。
Promise
側は本来、変数results
のスコープを閉じるためにさらにコード量を増やす必要がありますから、async/await
恐るべしです。
でも、マミさんが言ってました。
危険を冒してまで叶えたい願いがあるのかどうか、じっくり考えてみるべきだと思うの、って。
さあ、行こう。今夜もPromiseをやっつけないと。
次にPromise
とasync/await
で、並列実行と順次実行の混在を比較。
const results = {};
Promise.all([
Promise.resolve().then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge),
Promise.resolve().then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
])
.then(() => $.get('nyan.txt')).then(nyan => results.nyan = nyan)
.then(() => console.log(results.hoge + results.piyo + results.nyan));
(async() => {
const [hoge, piyo] = await Promise.all([
$.get('hoge.txt'),
$.get('piyo.txt')
]);
const nyan = await $.get('nyan.txt');
console.log(hoge + piyo + nyan);
})();
こちらも構成が似ていますね。そういう意味でasync/await
は、既存実装をより簡略化したものとして認識できるため、好感が持てます。
async/await
側ではES6
で導入された分割代入まで使われており、新しい技術のオンパレードです。Promise
側もアロー演算子をやめるとティロ・フィナーレですが。
Promise、アンタはいい子に育った。嘘もつかないし、悪いこともしない。
圧倒的と思われるasync/await
のシンプルさですが、例外が絡んでくるとPromise
のcatch
の流麗さも垣間見えます。
const results = {};
Promise.resolve()
.then(() => $.get('hoge.txt')).then(hoge => results.hoge = hoge)
.catch(e => results.hogeError = e)
.then(() => $.get('piyo.txt')).then(piyo => results.piyo = piyo)
.catch(e => results.piyoError = e)
.then(() => console.log(JSON.stringify(results)));
(async() => {
const results = {};
try {
results.hoge = await $.get('hoge.txt');
} catch (e) {
results.hogeError = e;
}
try {
results.piyo = await $.get('piyo.txt');
} catch (e) {
results.piyoError = e;
}
console.log(JSON.stringify(results));
})();
次にPromise
とasync/await
で、オブジェクトメソッドとの連携を比較。
// オブジェクトを用意
const Capsule = {
getHoge: function() {
return $.get('hoge.txt').then(hoge => Capsule.hoge = hoge);
},
getPiyo: function() {
return $.get('piyo.txt').then(piyo => Capsule.piyo = piyo);
},
out: function() {
console.log(Capsule.hoge + Capsule.piyo);
}
};
Promise.resolve()
.then(Capsule.getHoge)
.then(Capsule.getPiyo)
.then(Capsule.out);
(async() => {
await Capsule.getHoge();
await Capsule.getPiyo();
Capsule.out();
})();
前回記事でも書きましたが、Promise
はオブジェクト指向と相性が良いです。組み方によってはPromise
にも軍配が上がります。
非同期っぽく見えないはずのasync/await
は、ここではかえって非同期感を煽っています。await
がついている関数とついていない関数があり一体感もありません。メイン関数(ファサード部)のawait
を謎の呪文(async() => {})();
で囲う必要があるのも悲しいところ。
さやか、君がPromiseを覚えられない限り、杏子と戦っても、勝ち目は無いと思っていい。
こんなコードをよく見かけますよね。見かけないはずがない。
// データベースから取得する
function select(key, callback) {
db.select(key, (err, result) => {
if (err) {
return callback(err);
}
return callback(null, result);
});
}
// データベースから取得した値をクライアントに返す
select('key', (err, result) => {
if (err) {
res.send('ERR:' + err);
return;
}
res.send('OK:' + result);
});
これをPromise
で書き換えると、こんな感じです。よくある光景です。
// データベースから取得する
function select(key) {
return new Promise((resolve, reject) => {
db.select(key, (err, result) => {
if (err) {
return reject(err);
}
return resolve(result);
});
});
}
// データベースから取得した値をクライアントに返す
Promise.resolve()
.then(() => select('key'))
.then(result => res.send('OK:' + result))
.catch(err => res.send('ERR:' + err));
これをasync/await
で書き換えると…
// データベースから取得する
function select() {
// async/awaitへの書き換えはできない
}
// データベースから取得した値をクライアントに返す
(async() => {
try {
const result = await select('key');
res.send('OK:' + result);
} catch (err) {
res.send('ERR:' + err);
}
})();
Promise
には大きく分けて2種類あります。
- 非同期処理の終了を検知する検知側
- 非同期処理の処理順を記載する呼び出し側
呼び出し側はPromise
をasync/await
に書き換えられますが、検知側は書き換えられません。さやかちゃん?起きて…ねぇ、ねぇちょっと、どうしたの?
何なんだよ、Observableって一体何なんだ!?さやかに何をしやがった!?
Promise
では不可能なStream処理をやってくれるのがObservable
です。Observable
でも書いてみましょう。同期型のmapは難しいですが、今回はconcatMap
を使います。
const result = [];
of([])
.pipe(
concatMap(() => $.get('hoge.txt')),
map(v => result.push(v)),
concatMap(() => $.get('piyo.txt')),
map(v => result.push(v)),
concatMap(() => $.get('nyan.txt')),
map(v => result.push(v))
)
.subscribe(() => console.log(result));
Promise
の書き方とそっくりに書けました。
フロントサイドのAngularやReactは全面的にObservable
を採用しているので、たとえasync/await
でPromise
を撲滅しても、この書き方はまたやって来る。
第一級関数を忌み嫌う時、私達はグリーフシードになり、魔女として生まれ変わる。それが、魔法少女になった者の、逃れられない運命。
この街にはもう一人、async functionがいるからね
さやかちゃん、async function
は作れたって言ってるけど、でも、もしマミさんの時と同じようなことになったらって思うと。
async function hoge() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('hoge'), 1000);
});
}
async function fuga() {
const result = await hoge();
console.log(result);
}
fuga(); // 1秒後に'hoge'が表示される
はぁ…まどか、fuga
は本来のasync function
じゃなくて、ただの抜け殻なんだって。
hoge
が本来のasync function
で、fuga
はawait
を使うだけためのasync function
なんだよ。
これを見てごらん。
async function hoge() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('hoge'), 1000);
});
}
async function fuga() {
const result = await hoge();
console.log(result);
}
library.load(fuga).draw(); // draw()が即時実行されるか1秒待たされるか分からない
await
でhoge
を待ちたいという理由だけでfuga
をasync function
にしたとしても、library.load
がコールバックをawait
で待っていた場合、draw
がhoge
の終了までブロックされるという挙動に変更されるんだ。こればっかりは、library.load
の実装によりけりだね。
もしブロックされたくないなら、こうやって安全策を取らなきゃいけない。
async function hoge() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('hoge'), 1000);
});
}
function fuga() {
(async() => {
const result = await hoge();
console.log(result);
})();
}
library.load(fuga).draw(); // draw()は即時実行されて1秒後に'hoge'が表示される
さやかちゃんはさ、怖くはないの?後悔とか全然ないの?
美樹さやかが一生を費やして保守しても、バグが落ち着く日は来なかった。
実際の現場ではasync/await
が脆すぎる可能性はあります。
コーディング規約をちゃんとしないと、こんなことになる可能性も…。
async function selectUpdate() {
return update(await select());
}
selectUpdate()
.then(o => res.send(o))
.catch(e => res.send(e.message));
selectUpdate
が開発途中でasync function
に変更されても受ける側はthen
のままだったり、await
と混在してしまったり…というのはマシな方です。
恐いのはupdate
関数の呼び出しにawait
がついてないのが、ミスかどうかすぐには分からないところ。今は大丈夫でもupdate
関数の仕様が途中で変更される可能性もあります。もしこれがバグの原因となり品質管理チームにバレてしまうと、「関数呼び出し時には必ずawait
をつける」というコーディング規約が爆誕し、多くの犠牲者が出るかもしれません。
そんなルールは有り得ないと言いたいところですが、悲しいことにPromise
のthen
による関数呼び出しは全て「await
をつけて呼び出している状態」になっており、それはある意味で安全なのです。
Promise.all
であえて非同期にしない限り、Promise
で書くと全て同期処理された。だけどasync/await
にすると、await
の付け方で思いもよらない非同期処理が生まれてしまう。私達魔法少女って、そう言う仕組みだったんだね。
あたしって、ホントばか。
おしまい