文字列のコードを非同期実行するときのTips

JavaScriptにはeval関数が用意されており、これを使う事で、生成したプログラムを実行することができます。

それで、[日本語プログラミング言語「なでしこ」](https://nadesi.com)も、日本語で書かれたプログラムをJavaScriptに変換してからeval的に実行しています。このように、JavaScript生成系スクリプトでは、evalが役立ちます。

ところが、evalで実行するプログラムを非同期で実行するとき、それなりに気をつかう場面があります。JavaScriptの非同期処理は、うまく対応させないと難しく、エラーになったり、実行されるものの非同期に実行されなかったり、await以後が実行されなかったり…とはハマりポイントが満載です。

# eval内でも非同期実行が可能

まず、eval内でも非同期実行が可能であることを確かめてみましょう。

```js:call_eval_str_async.js
// 非同期関数を定義
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関数を実行できます。

```js:call_func_str_async.js
// 文字列で書いた非同期関数の呼び出し
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関数を戻すようにすれば良さそうですが、以下のコードはエラーになります。

```js
// エラーになるコード
const src = `
  function wait() { ... }
  return (async () => {
    await wait();
  });
`
(async () => {
  const f = new Function(src);
  await f()
})();
```

# evalとevalの外側を同時に使う

```js:hukugouwaza.js
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)を実行するようにしたい場合は、次のように記述します。

```js:hukugouwaza_kai.js
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の内容を非同期で待機させることができます。

```js
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すれば、それっぽく動きます。

# 参考

https://stackoverflow.com/questions/56187117/await-is-only-valid-in-async-function-eval-in-async