3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

この記事誰得? 私しか得しないニッチな技術で記事投稿!

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

Last updated at Posted at 2023-06-25

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

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

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

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

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

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関数を実行できます。

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関数を戻すようにすれば良さそうですが、以下のコードはエラーになります。

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

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

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

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

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

参考

3
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?