😽

[JavaScript] - 画像読込が完了してから次の処理に進ませる(同期的に読込む)方法

2023/09/27に公開

はじめに

私は C/C++ のような手続型言語(構造化プログラミング)出身なので、JavaScript のような非同期処理が前提のプログラムは感覚が違って慣れてないというか、それはそれで面白いというか、苦労しているところです。
といっても私はプログラマではないので、それなりのレベル止まりですが・・・。

今回も自分の備忘録です。
結局以下の内容は、

    処理完了を待って値を返す関数 を作る

ということですが、画像読込を例に書いています。

動機

JavaScript で Canvas に画像を読み込んで画像を操作したい時があります。
でも JavaScript は非同期動作をするので、以下のように同期プログラムの要領で書くと画像読込中に画像操作の処理が走ってしまいます。


 const img = new Image();
 img.src = "./hoge/fuga.png";

 // ↑ 上の画像読込処理が終わる前に
 // ↓ 以下の処理が走る

 ctx.drawImage(img, 0, 0);

 let img_data = ctx.getImageData(0, 0, canvas.width, canvas.height);

 //画素の処理
 for(let i = 0; i < canvas.height; i++){
    for(let j = 0; j < canvas.width; j++){
        let n = i * canvas.width + j;
        for(let k = 0; k < 4; k++){
            img_data.data[4 * n + k] = piyo(i, j, k);   //ここでエラー
        }
    }
 }

これを避けて画像の読込完了後に処理を実行させるためには onload や addEventListener を使用して書くように、と、いろんなところで説明されています。


 const img = new Image();
 img.src = "./hoge/fuga.png";
 
 //読込完了後に実行する関数を設定
 img.onload = () => {
    ctx.drawImage(img, 0, 0);
    let img_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    //画素の処理
    for(let i = 0; i < canvas.height; i++){
        for(let j = 0; j < canvas.width; j++){
            let n = i * canvas.width + j;
            for(let k = 0; k < 4; k++){
                img_data.data[4 * n + k] = piyo(i, j, k);   //ここでエラー
            }
        }
    }
 }

これで済む場合はこれでいいと思うのですが、そうじゃない時もあります。
例えば画像読込完了しないとそれ以降のすべての処理を始められないような場合。
以降の処理すべてを関数に詰め込んで onload に代入するわけにはいきません。
うまくアルゴリズムを調整すればいいのかもしれませんが、記述の自由度が下がります。

でも img.src = "./hoge/fuga.png"await 使えないんですよね。

× await img.src = "./hoge/fuga.png"
× img.src = await "./hoge/fuga.png"

そんな時に使える小ネタを記載しておきます。

出典はこちらのページから。
ありがとうございます。

https://pisuke-code.com/js-load-image-synchronously/

https://zenn.dev/nana/articles/ff5c9db4132e9d

コード

以下のように画像読込関数を定義して使用すればいいです。

//画像読込関数
async function load_image(path){
    const t_img = new Image();
    return new Promise(
        (resolve) => {
            t_img.onload = () => {
                resolve(t_img);
            }
            t_img.src = path;
        }
    )
}

//関数使用
let img = await load_image("./hoge/fga.png");

//上の画像読込が完了するまで(変数:img に画像が読込完了するまで)この下に処理がすすまない。

これで、画像が完全に読込まれるまで次の処理に進まないようにできます。
他にも応用が利きますね。

中身の説明

以下、
 「なんでこれでできるの? どういう内容になってるの?」
ということの説明です。

「コード(やり方)だけ載せて終わり」にしてるページもありますが、「これ、どう理解すればいいの? なんで? どうして?」と思う時が自分は多いです。
説明がないので自分でその理由を探すのですが、検索しても同じページが引っ掛かって結局わからない、みたいなことが結構あって困ります。
テック記事なら説明書いておいてよ・・・と思う事が多くて困るのと、テック記事として説明がないのは「片手落ち」だと思うので説明を書いておこうと思います。
(間違ってたら誰かが優しく教えてくれる!)

まず、

  • Promise
  • async, await

については今回の本筋から逸れるので、別のページに解説をお願いしちゃいます。
JavaScript 書く時には必須の知識。
こちらとかわかりやすいです。

https://qiita.com/cheez921/items/41b744e4e002b966391a

関数構成

関数の構成は単純です。

  1. まず await で使用したいので async で関数を作成します。
  2. 引数に画像の存在場所の path を取ってその画像を読み込むようにしています。
  3. 関数内で new Image() してImageオブジェクトを作ります。
  4. async で関数を作ったので、Promise をリターンしてやります。
async function load_image(path){
    const t_img = new Image();
    return new Promise();
}

です。

Promise の中身

返す Promise の中で画像を読込んでやります。
画像は Promise Value で渡します。

Promise のコンストラクタには resolve関数reject関数 を引数に持つ関数を渡してやらなければなりません。
(C/C++ 的に解釈すれば「2つの関数ポインタ仮引数を持つ関数をコンストラクタに渡す」となる)
なので、2つの仮引数を持つ関数をコンストラクタに渡してやります。

function executor(resolve, reject){
    //何か処理
}

第1引数の文字が resolve 時に実行される関数(resolve関数)の名前に、第2引数の文字が reject 時に実行される関数(reject関数)の名前に自動的に設定されます。
(仮引数なので、C/C++的にはまあ当たり前です)
なので、別に hoge でも fuga でもいいです。

function executor(hoge, fuga){
    //何か処理
}

このexecutor関数内に画像読込の処理を書きます。
書く処理は2つです。

  1. 画像が読み込まれた後に実行する処理 onload
  2. 読込元pathを指定しての画像読込

onload には読込が終わったImageオブジェクトを返す処理を書いて設定してやります。
つまり、Promise Value にImageオブジェクトをセットする処理を書きます。
Promise Value にセットするには、resolve関数 にオブジェクトを渡してやればいいです。
なので以下になります。

function executor(resolve, reject){
    t_img.onload = function(){      //1. 画像が読み込まれた後に実行する処理
        resolve(t_img);
    }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
}

繰り返しですが、関数名を変えて以下のように書いても同じです。

function executor(hoge, fuga){
    t_img.onload = function(){      //1. 画像が読み込まれた後に実行する処理
        hoge(t_img);
    }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
}

仮引数は省略できます。
今回は成功時の結果しか要らないので第2引数を省略します。

function executor(resolve){
    t_img.onload = function(){      //1. 画像が読み込まれた後に実行する処理
        resolve(t_img);
    }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
}

このexecutor関数を Promise のコンストラクタに渡してやります。

function executor(resolve){
    t_img.onload = function(){      //1. 画像が読み込まれた後に実行する処理
        resolve(t_img);
    }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
}

new Promise(executor);

return に書くために、executor関数をカッコの中に直接書きます。

new Promise(function executor(resolve){
    t_img.onload = function(){      //1. 画像が読み込まれた後に実行する処理
        resolve(t_img);
    }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
});

executor という関数名は別に要らないので無名関数にしてやります。
さらに、今流行りのラムダ式(アロー関数)にしてやります。

new Promise(
    (resolve) => {
        t_img.onload = () => {      //1. 画像が読み込まれた後に実行する処理
            resolve(t_img);
        }
    t_img.src = path;               //2. 読込元pathを指定しての画像読込
    }
);

こうして作った Promise を return してやります。

async function load_image(path){
    const t_img = new Image();
    return new Promise(
        (resolve) => {
            t_img.onload = () => {      //1. 画像が読み込まれた後に実行する処理
                resolve(t_img);
            }
        t_img.src = path;               //2. 読込元pathを指定しての画像読込
        }
    );
}

できました。

受け側(使用側)

async 関数で作ったので await で使用して受けてやります。

 let img = await load_image("./hoge/fga.png");

load_image関数 の戻り値は Promiseオブジェクト ですが、await をつけることで Promise Value を受け取ることができます。
何を言いたいかというと、await があるなしで返ってくる結果が違います。

 let img = await load_image("./hoge/fga.png");  //imageオブジェクトが返る
 let img = load_image("./hoge/fga.png");        //Promiseオブジェクトが返る

なので、await をつけない場合、以下のようにしないとImageオブジェクトを取り出せません。

let img;
let p_img = load_image("./hoge/fga.png"); 
await Promise.all([
    p_img.then(
        (result) => {
            img = result;
        }
    )
]);

もしくは、

let p_img = load_image("./hoge/fga.png"); 
let img = await p_img.then(
    (result) => {
        return result;
        }
)

これだと一体何がしたかったのか分からなくなるので、素直に await 付けて受け取ってください。

まとめ

処理完了を待って値を返す関数 を作りたい場合は、

  1. asyncPromise をreturnする関数を作る。
  2. return する Promise 内で処理を実行する。
  3. 返したい値(オブジェクト等)を resolve関数resolve Value にセットする。
  4. await で受けて変数に格納する。

といった手順で実行できます。

以上です。
間違い等ありましたらお知らせください。

Discussion