🤖

js で esmodule下であるならば top level await が有効となっているのだから無限ループで 処理してもいいよね

2024/11/12に公開

というわけで javascript のアイディアの話です。 setTimeout でループを書いている旧世代の処理を 無限ループで書いてみようという話となります。

setTimeout のループ

具体的にはこれまではこう書いていたところを

function loopAction() {
  // doing
  setTimeout(loopAction, 500);
}
loopAction();

こう書いてみませんか?

while(true) {
  // doing
  await new Promise(resolve => setTimeout(resolve, 500));
}

という話です。

これはいろいろと応用が利きまして

画像の読み込み

画像の読み込みを こう書いていたものを

const srcUrl = "画像のファイルパス";
const image = new Image();
image.addEventListener('error', ({error}) => {
   // doing error process
});
Image.addEventListener('load',({target:image}) => {
  // doing load complete process
}
Image.src = srcUrl;

こう書くことができます。

const srcUrl = "画像のファイルパス";
try {
  const image = await new Promise((resolve, reject) => {
    const image = new Image();
    image.addEventListener('error', ({error}) => reject(error));
    image.addEventListener('load', ({target}) => resolve(target));
    image.src = srcUrl;
  });
  // doing load complete process
}  catch (error) {
  // doing error process
}

この Image を使いまわししたいとなるなら次の様に書いたらいいと思います。
addEventListener の options に signal を指定すると abort 時に解除してくれます。

const srcUrl = "画像のファイルパス";
try {
  const controller = new AbortController();
  let image;
  try {
    const signal = controller.signal;
    image = await new Promise((resolve, reject) => {
      signal.throwIfAborted()
      signal.addEventListener('abort', ({target}) => reject(target.reason));
      const image = document.getElementById('使いまわす image の id');
      image.addEventListener('error', ({error}) => reject(error), {signal});
      image.addEventListener('load', ({target}) => resolve(target), {signal});
      image.src = srcUrl;
    });
  } finally {
    controller.abort();
  }
  // doing load complete process
} catch (error) {
  // doing error process
}

https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener#options

これによりイベントは処理後にちゃんと解除までされます。

もしも let image が気持ち悪いのであれば次の様に書いても同様の動作となります。

const srcUrl = "画像のファイルパス";
try {
  const controller = new AbortController();
  const signal = controller.signal;
  const image = await new Promise((resolve, reject) => {
    signal.throwIfAborted()
    signal.addEventListener('abort', ({target}) => reject(target.reason));
    const image = document.getElementById('使いまわす image の id');
    image.addEventListener('error', ({error}) => reject(error), {signal});
    image.addEventListener('load', ({target}) => resolve(target), {signal});
    image.src = srcUrl;
  }).finally(() =>  controller.abort());
  // doing load complete process
} catch (error) {
  // doing error process
}

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally

Promise の finally は戻り値を変更せずに resolve / reject どっちの場合でも処理されるメソッドな為、こういう際に利用できます。

ファイルの読み込み

ファイルの読み込みも昔は下記の様に書いていたのですが

const file = event.target.files[0];
const reader = new FileReader();
reader.addEventListener('error', ({target: {error}}) => {
  // doing error process
});
reader.addEventListener('load', ({target: {result}}) => {
  // doing load complete process
});
reader.readAsArrayBuffer();

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsArrayBuffer

今ならこう書けます。

const file = event.target.files[0];
try {
  const result = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('error', ({target: {error}}) => reject(error));
    reader.addEventListener('load', ({target: {result}}) => resolve(result));
    reader.readAsArrayBuffer();
  });
  // doing load complete process
} catch (error) {
  // doing error process
}

いえ、 そもそも Blob に arrayBuffer() が生えているので これでいいです。

const file = event.target.files[0];
try {
  const result = await file.arrayBuffer();
  // doing load complete process
} catch (error) {
  // doing error process
}

https://developer.mozilla.org/ja/docs/Web/API/Blob/arrayBuffer

以上。

Discussion