[JavaScript] - 画像読込が完了してから次の処理に進ませる(同期的に読込む)方法
はじめに
私は 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"
そんな時に使える小ネタを記載しておきます。
出典はこちらのページから。
ありがとうございます。
コード
以下のように画像読込関数を定義して使用すればいいです。
//画像読込関数
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 書く時には必須の知識。
こちらとかわかりやすいです。
関数構成
関数の構成は単純です。
- まず
await
で使用したいのでasync
で関数を作成します。 - 引数に画像の存在場所の
path
を取ってその画像を読み込むようにしています。 - 関数内で
new Image()
してImageオブジェクトを作ります。 -
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つです。
- 画像が読み込まれた後に実行する処理
onload
- 読込元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
付けて受け取ってください。
まとめ
処理完了を待って値を返す関数 を作りたい場合は、
-
async
でPromise
をreturnする関数を作る。 - return する
Promise
内で処理を実行する。 - 返したい値(オブジェクト等)を
resolve関数
でresolve Value
にセットする。 -
await
で受けて変数に格納する。
といった手順で実行できます。
以上です。
間違い等ありましたらお知らせください。
Discussion