JavaScriptにはeval関数が用意されており、これを使う事で、生成したプログラムを実行することができます。
それで、日本語プログラミング言語「なでしこ」も、日本語で書かれたプログラムをJavaScriptに変換してからeval的に実行しています。このように、JavaScript生成系スクリプトでは、evalが役立ちます。
ところが、evalで実行するプログラムを非同期で実行するとき、それなりに気をつかう場面があります。JavaScriptの非同期処理は、うまく対応させないと難しく、エラーになったり、実行されるものの非同期に実行されなかったり、await以後が実行されなかったり…とはハマりポイントが満載です。
eval内でも非同期実行が可能
まず、eval内でも非同期実行が可能であることを確かめてみましょう。
// 非同期関数を定義
function wait() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('wait')
resolve()
}, 1000)
})
}
// 文字列で書いた非同期関数の呼び出し
const src = `
(async () => {
await wait()
await wait()
await wait()
})()
`
eval(src)
これは文字列の中で非同期関数を呼び出します。しかし、その際、上記のように、(async () => { 処理 })()
のように、無名async関数の定義と呼び出しを記述する必要があります。
new Function を使う場合
上記と全く同じですが、new Functionでスコープを限定して評価する場合も同じです。文字列内で定義したwait関数を実行できます。
// 文字列で書いた非同期関数の呼び出し
const src = `
// ---
// 非同期関数を定義
function wait() {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('wait')
resolve()
}, 1000)
})
}
(async () => { await wait(); await wait(); await wait() })();
// ---
`
const f = new Function(src)
f()
eval/Functionで世界は閉じている
注意点としては、eval関数自体はasyncでも何でもないので、evalの中か外で切り分けないといけないところです。
残念ながら、async関数を戻すようにすれば良さそうですが、以下のコードはエラーになります。
// エラーになるコード
const src = `
function wait() { ... }
return (async () => {
await wait();
});
`
(async () => {
const f = new Function(src);
await f()
})();
evalとevalの外側を同時に使う
function wait(who) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`wait:${who}`);
resolve();
}, 1000);
});
}
const src = `(async () => {
console.log('<src>')
await wait('src1');
await wait('src2');
console.log('</src>')
})();`
async function foo() {
for (let i = 0; i < 3; i++) {
eval(src); // --- (*1)
await wait('outer'); // --- (*2)
}
}
foo();
ただし、上記のようにawaitを使いつつ、eval内でawaitを実行することも可能です。
それでも、期待通り(*1)のeval後に(*2)が実行されるわけではありません。(*1)と(*2)は同時に実行されてしまいます。上記の実行結果は次の通りです。
<src>
wait:src1
wait:outer
<src>
wait:src2
</src>
wait:src1
wait:outer
<src>
wait:src2
</src>
wait:src1
wait:outer
wait:src2
</src>
しかし、(*1)の次に(*2)を実行するようにしたい場合は、次のように記述します。
function wait(who) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`wait:${who}`);
resolve();
}, 1000);
});
}
const src = `
var EVAL_F = (async () => {
console.log('<src>')
await wait('src1');
await wait('src2');
console.log('</src>')
});
`
async function foo() {
eval(src)
for (let i = 0; i < 3; i++) {
await EVAL_F(); // --- (*1)
await wait('outer'); // --- (*2)
}
}
foo();
上記のように、evalの中で変数EVAL_Fを定義、evalの外で、EVAL_Fをawaitで実行することで、(*1)の次に(*2)を実行できるようになります。以下は実行結果です。
<src>
wait:src1
wait:src2
</src>
wait:outer
<src>
wait:src1
wait:src2
</src>
wait:outer
<src>
wait:src1
wait:src2
</src>
wait:outer
evalをasync関数に閉じ込める方法
なお、コメント欄で @shiracamus 様に教えてもらったのですが、上記evalの戻り値はPromiseなので、これをawaitすることで、evalの内容を非同期で待機させることができます。
function wait(who) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`wait:${who}`);
resolve();
}, 1000);
});
}
const src = `
(async () => {
console.log('<src>')
await wait('src1');
await wait('src2');
console.log('</src>')
})();
`
async function evalSrc() {
await eval(src) // evalから返ってきたPromiseをawaitする
// return eval(src) // こっちの場合はevalSrc関数自体はasync関数じゃなくてもOK
}
async function foo() {
for (let i = 0; i < 3; i++) {
await evalSrc(); // --- (*1)
await wait('outer'); // --- (*2)
}
}
foo();
実行すると、次のようになります。
<src>
wait:src1
wait:src2
</src>
wait:outer
<src>
wait:src1
wait:src2
</src>
wait:outer
<src>
wait:src1
wait:src2
</src>
wait:outer
まとめ
基本的には、evalの中と外は個別にasync/awaitを記述する必要があります。ただし、例外的にevalの中で定義したasync関数の変数をawaitするか、Promise自体を戻してawaitすれば、それっぽく動きます。
参考