JavaScriptのPromise.all()メソッドを使い、すべての画像の読み込みが完了した時に何か処理をする方法を紹介します。主にCanvas APIのdrawImage()メソッドを使う際、指定する画像がすでに読み込まれている必要があるため重宝する仕組みになります。
やりたいことの確認


「green.png」「pink.png」という2枚の画像ファイルが、imgフォルダの中にあるとします。

それらの画像をjsにて読み込み、こちらのキャンバスに描画したい場合を考えます(CSSにて背景色を水色に設定しています)。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const names = ['green', 'pink'];
names.forEach((name, i) => {
const img = new Image();
img.src = `img/${name}.png`;
ctx.drawImage(img, i * 80, 0);
});
上記のコードでは、img要素のsrc属性を指定した後、画像が読み込まれる前にすぐにdrawImage()しているため上手くいきません。
addEventListener()のloadイベントを使った方法
img要素にsrc属性を指定する前に、img要素にloadイベントを追加することで解決します。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const names = ['green', 'pink'];
names.forEach((name, i) => {
const img = new Image();
img.addEventListener('load', () => {
ctx.drawImage(img, i * 80, 0);
});
img.src = `img/${name}.png`;
});

loadイベント内でdrawImage()することで、画像の読み込みが完了した後に描画するため上手くいきます。
しかし画像を重ねて描画する場合、先に読み込まれたものから描画されるため、画像サイズの違いによって重なり順が入れ替わる場合があります。描画処理を以下のように変更します。
ctx.drawImage(img, i * 20, i * 20);


このように、どちらの結果になるかが定まらないような事態が発生します(実際にはこの程度のサイズの画像ではすぐに読み込まれるため、このようなことは起きにくいです)。このような問題を解決するため、すべての画像の読み込み完了を待って、まとめて描画する必要があります。
Promise.all()ですべての画像の読み込み完了を検知
非同期処理のPromise.all()を使うと、すべての読み込み完了を待つことができます。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const names = ['green', 'pink'];
draw();
async function draw() {
const imgs = await Promise.all(names.map(name => new Promise(resolve => {
const img = new Image();
img.addEventListener('load', () => resolve(img));
img.src = `img/${name}.png`;
})));
// すべての画像の読み込み完了後に行う処理
imgs.forEach((img, i) => {
ctx.drawImage(img, i * 20, i * 20);
});
}

Promise.all()は非同期処理のため、非同期関数内で使う必要があります。画像の読み込みを含めた描画処理をdraw()関数にまとめ、asyncの接頭辞をつけることで非同期関数にすることができます。
await Promise.all()とすることで、引数に指定したPromiseオブジェクトの配列のすべての処理が完了するまで、非同期的に処理を滞らせることができます。Promise.all()は各処理で返された値を配列にして返すことができるため、作成したimg要素の配列をimgsに代入します。
読み込み待ちをする各Promiseオブジェクトは、namesの要素を元に作成したいので、namesに対してmap()を使い、Promiseオブジェクトの配列を作成します。
プロミスオブジェクトを作るためのnew Promise()の引数にはすぐに実行する関数を渡します。その関数の中でresolve()を実行することで、処理の完了を知らせます。loadイベントが発火することで処理の完了を知らせるためresolve()しますが、その時に引数で返り値を渡せるため、作成したimg要素を指定します。
返り値を使わない、Promise.all()のシンプルな使い方
以下の方法は、Promise.all()からの返り値を取得しないためシンプルで理解しやすいかと思います。
const names = ['green', 'pink'];
const imgs = [];
fn();
async function fn() {
await Promise.all(names.map(name => new Promise(resolve => {
const img = new Image();
imgs.push(img);
img.addEventListener('load', resolve);
img.src = `img/${name}.png`;
})));
// すべての画像の読み込み完了後に行う処理
}
imgsに作成したimg要素を取得し、すべての画像の読み込み完了を検知します。
Promise.all()からの返り値を使用しないため、resolve()に引数を指定する必要もありません。