asyncとawaitの仕様を調べます。EcmaScript 2017(8th Edition)で追加された機能です。
基本的な使い方
awaitは、Promiseオブジェクトを返す関数を並べて実行する、つまり同期を取りつつ実行するのに使うようです。ただし、awaitはasyncを付けた関数の中でしか使えません。
次の例では、Promiseを返すmakePromiseを並べて、前にawaitを付けています。await makePromise(a)
の実行が終わってから、await makePromise(b)
が順に実行されるはずです。
function makePromise(num) {
return new Promise((resolve, reject) => {
if(num % 2 == 0)
resolve('OK');
else
reject('NG');
console.log(`executer called: ${num}`);
});
}
(async (a, b) => {
let x = await makePromise(a);
let y = await makePromise(b);
console.log(`async function finished: ${x} ${y}`);
})(2, 4);
console.log("async function called");
executer called: 2
async function called
executer called: 4
async function finished: OK OK
アロー関数が読みにくければ、次のように書いても同じです。
let afunc = async function(a, b) {
let x = await makePromise(a);
let y = await makePromise(b);
console.log(`async function finished: ${x} ${y}`);
};
afunc(2, 4);
console.log("async function called");
普通の関数定義の前にasyncを付けることもできます。
async function afunc(a, b) {
let x = await makePromise(a);
let y = await makePromise(b);
console.log(`async function finished: ${x} ${y}`);
}
afunc(2, 4);
console.log("async function called");
awaitの働き
上記の例では、「executer called: 2」が実行されて少し経ってから「executer called: 4」が実行されています。async関数の中では順番に動いていますが、外側から見ると非同期的な動きです。
awaitは次の働きをしているようです。
- Promiseオブジェクトの前にawaitを付けると、履行済みになるまで待つ。
- 「await Promiseオブジェクト」は履行済み結果の値を返す。
thenやcatchはPromiseオブジェクトを返すので、次のようにするとxに"OKOK"
が入ります。
let x = await makePromise(a).then(x => x + x);
async関数の中にさえあれば、awaitはどんな式の前にでも付けられるようです。次にようにしてもエラーになりません。zにはtrueかfalseが入ります。
let z = await (a == b);
履行済みになるまで待つので、次のようにresolveを呼ばない場合は、2つ目のmakePromiseはいつまでも実行されません。async関数を呼び出しても、async関数の中身の実行は終了しません。
function makePromise(num) {
return new Promise((resolve, reject) => {
console.log(`executer called: ${num}`);
});
}
(async (a, b) => {
let x = await makePromise(a);
let y = await makePromise(b);
console.log(`async function finished: ${x} ${y}`);
})(2, 4);
console.log("async function called");
executer called: 2
async function called
asyncが返すもの
次のように、asyncを付けて作った関数が返すものと、その関数呼び出しが返すものを調べます。
let afunc = async (a, b) => {
};
console.log(afunc.constructor);
console.log(afunc(2, 4).constructor);
ƒ AsyncFunction() { [native code] }
ƒ Promise() { [native code] }
asyncを付けて作った関数は、FunctionオブジェクトではなくAsyncFunctionオブジェクトになります。Functionと違って、AsyncFunctionではnew AsyncFunction()
とすると「ReferenceError: AsyncFunction is not defined」が出ます。つまり、AsyncFunctionという名前は内部的にだけ使われています。
async関数に()を付けて呼び出すと、Promiseオブジェクトが返ります。ということは、async関数の呼び出しに対してthenやcatchが使えるということです。
次の例は、関数makeAsyncPromiseでasyncが作ったPromiseを返し、そのPromiseに対してthenとcatchをつなげています。async関数の戻り値が、履行済み結果の値になることが分かります。
function makePromise(num) {
return new Promise((resolve, reject) => {
if(num % 2 == 0)
resolve('OK');
else
reject('NG');
console.log(`executer called: ${num}`);
});
}
function makeAsyncPromise(a1, b1) {
return (async (a2, b2) => {
let x = await makePromise(a2);
let y = await makePromise(b2);
return x + y;
})(a1, b1);
}
makeAsyncPromise(2, 4).then((x) => {
console.log(`then callback: ${x}`);
}).catch((r) => {
console.log(`catch callback: ${r}`);
});
console.log('then and catch called');
executer called: 2
then and catch called
executer called: 4
then callback: OKOK
例外
async内では、try - catchを使って例外またはrejectの結果を処理できます。
(async (a, b) => {
try {
let x = await makePromise(a);
let y = await makePromise(b);
console.log(`async function finished: ${x} ${y}`);
}
catch(e) {
console.log(`error: ${e}`);
}
})(2, 1);
executer called: 2
executer called: 1
error: NG
async関数が返すPromiseに対してcatchを呼べば、try - catchを使う必要はなくなります。executerのrejectや、コールバック関数、async関数で発生した例外は、すべて最後のcatchで処理できます。
function makeAsyncPromise(a1, b1) {
return (async (a2, b2) => {
let x = await makePromise(a2);
let y = await makePromise(b2);
return x + y;
})(a1, b1);
}
makeAsyncPromise(2, 1).then((x) => {
console.log(`then callback: ${x}`);
}).catch((r) => {
console.log(`catch callback: ${r}`);
});
console.log('then and catch called');
executer called: 2
then and catch called
executer called: 1
catch callback: NG
すべてを同期するのは無理
ここまで長々と書いてきたのは、最近Puppeteerの使い方を調べたときに、async、awaitがよく分からなかったからです。今どきのJavaScriptライブラリではPromiseオブジェクトを返す関数が用意されてて、awaitを付けて使うものらしいですね。
次のような書き方で「1→2→3」の順に実行することはできないのか、というのが研究の目的でしたが、できないことが分かりました。
console.log("1");
(async (a, b) => {
let x = await makePromise(a);
let y = await makePromise(b);
console.log("2");
})(2, 4);
console.log("3");
asyncとawaitを使っても、結局のところ非同期で動くPromiseが次々にできていくだけなので、最終的にはコールバック関数を使うしかありません。JavaScriptの1つの流れの中でPromiseすべてを順番に動かすのは無理です。
console.log("1");
(async (a, b) => {
let x = await makePromise(a);
let y = await makePromise(b);
console.log("2");
})(2, 4).then((x) => {
console.log("3");
});
以上です。気が向いたら追加するかもしれません。