質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.52%

JavaScriptの関数内の処理順序について

解決済

回答 2

投稿

  • 評価
  • クリップ 2
  • VIEW 5,549

tarotarosu

score 112

前提・実現したいこと

Canvasを用いて画像の合成処理を行っています。
頻繁に行う処理であるため以下のように関数化しました。

該当のソースコード

//配列内に格納された画像を順に合成していく関数
function synthesize_img(arr, color, shade, canvas_id) { 
  width = 580;
  height = 1620;

  var canvas = document.getElementById(canvas_id);
  var context = canvas.getContext("2d");
  context.clearRect(0, 0, width, height);
  var images = [];
  //配列内に画像パスを格納していく
  for(var i in arr){
    images[i] = new Image;
    images[i].src = arr[i];
  }
  //最後尾に画像を格納
  var last = images.length;
  images[last] = new Image;
  images[last].src = color;
  //更に最後尾に画像を格納
  var last2 = images.length;
  images[last2] = new Image;
  images[last2].src = shade;

  //配列内のすべての画像の読み込みが終わった時点で、画像の合成と描画を行う
  var loadedCount = 1;
  for(var i in images){
    images[i].addEventListener("load", function(){
      if(loadedCount == images.length){
        //配列の最後尾の前までを合成
        for(var j=0; j<images.length-1; j++){
          context.globalCompositeOperation = "source-over";
          context.globalAlpha = 1.0;
          context.drawImage(images[j], 0, 0, width, height);
        }
        //配列の最後尾を乗算
        context.globalCompositeOperation = "multiply";   //IEでは対応していない
        context.drawImage(images[images.length-1], 0, 0, width, height);
        //必要に応じて合成
        if(arr != SRC_ARR.back && arr != SRC_ARR.right && arr != SRC_ARR.left){
          var img_add = new Image;
          img_add.src = btn_front;
          img_add.addEventListener("load", function(){
            context.globalCompositeOperation = "source-over";
            context.globalAlpha = 1.0;
            context.drawImage(img_add, 0, 0, width, height);
            //処理終了確認
            console.log("Draw Finish!");
          });
        }
      }
      loadedCount++;
    }, false);
  }
  //処理終了確認
  console.log("All Finish!");
}

JavaScriptはシングルスレッドで処理されていくため、上記のような非同期処理を含まない場合、上から順に処理が行われていくのが普通だと思います。
しかし、実行しChromeのコンソールを見てみると、「All Finish!」が先に表示され、「Draw Finish!」が後に表示されます。
この原因は一体何なのでしょうか?また、これを解決するにはどうすればよいのでしょうか?(やはり、deferredを使用することでしょうか?)

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+5

Deferred+Promiseパターンを使っても構いませんが,Promise単独で使うパターンのほうが主流ですね.前者はjQueryなどのライブラリに依存しますが,後者はECMAScript2015という最新のJavaScriptの仕様の一部となっており,Chrome,Firefox,Safari,EdgeなどのWebブラウザなら標準で使うことができます.(IEを除く)

せっかくなので,古く感じられる部分をES2015ベースの書き方になおしてみます.

'use strict';

/**
 * 画像を非同期で読み込む関数
 * @param {string} source - 画像URL
 * @return {Promise} Imageをthenコールバックの第1引数として渡すPromise
 */
function loadImageAsync(source)
{
    return new Promise((resolve, reject) => {
        const image = new Image;
        const onLoad = () => {
            image.removeEventListener('load', onLoad);
            image.removeEventListener('error', onError);
            resolve(image);
        }; 
        const onError = () => {
            image.removeEventListener('load', onLoad);
            image.removeEventListener('error', onError);
            reject(new Error('Failed to load image: ' + source));
        }; 
        image.addEventListener('load', onLoad);
        image.addEventListener('error', onError);
        image.src = source;
    });
}

/**
 * 画像を合成する関数
 * @param {Array} imageSources - 本体画像のURLの配列
 * @param {string} colorSource - 色付けに用いる画像のURL
 * @param {string} shadeSource - シェーディングに用いる画像のURL
 * @param {string} canvasId - canvas要素のid属性値
 * @return {Promise} 次の処理に繋げるためのPromise
 */
function synthesizeImage(imageSources, colorSource, shadeSource, canvasId)
{ 
    // 幅,高さ,コンテキストを定義
    const width = 580;
    const height = 1620;
    const context = document.getElementById(canvasId).getContext('2d');
    context.clearRect(0, 0, width, height);

    // すべてを読み込み終わった後にthenコールバックを実行
    // そしてこの関数の返り値もPromiseにし,「synthesizeImageが終わった後の処理」を外部からも書けるようにする
    return Promise
    .all([shadeSource, colorSource, ...imageSources].map(loadImageAsync))
    .then(images => {
        // 配列を分解代入 (colorSourceとimageSourcesに対応するImageはotherImagesにまとめる)
        const [shadeImage, ...otherImages] = images;

        // otherImagesの全要素についてsource-overで描画を実行
        for (const image of otherImages) {
            context.globalCompositeOperation = 'source-over';
            context.globalAlpha = 1.0;
            context.drawImage(image, 0, 0, width, height);
        }

        // shadeImageについてmultiplyで描画を実行
        context.globalCompositeOperation = 'multiply';
        context.drawImage(shadeImage, 0, 0, width, height);

        if (sources === SRC_ARR.back) return;
        if (sources === SRC_ARR.right) return;
        if (sources === SRC_ARR.left) return;

        // 条件を満たすとき,BTN_FRONTを読み込んだ後,描画を実行し,Draw Finish!と表示する
        // そして,次のthenコールバックをこのPromiseの後に続けるためにreturnする
        return loadImageAsync(BTN_FRONT).then(image => {
            context.globalCompositeOperation = 'source-over';
            context.globalAlpha = 1.0;
            context.drawImage(image, 0, 0, width, height);
            console.log('Draw Finish!');
        });
    });
}
  • btn_frontはグローバル変数のようなので,それと分かるようにSRC_ARR同様すべて大文字にしました.
  • 上を除き,原則的にJavaScriptではcanvas_idのようにスネークケースではなくcanvasIdのようにキャメルケースにすべて統一する文化のようです.

これを呼び出すときは,以下のように必ずエラーに対応する処理も併せて書きます.

'use strict';

synthesizeImage(/* ...(適宜引数を入れる) */)
.then(() => console.log('All Finish!!')) // 最後にここが実行される
.catch(e => console.error(e.stack || e)); // エラー時はここが実行される

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/07/26 15:46

    備考:

    旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section5 ~ES2015文法を覚えよう(前編)~
    http://qiita.com/gaogao_9/items/18b20ad9b76c9c81b5fa

    JavaScriptは如何にしてAsync/Awaitを獲得したのか Qiita版
    http://qiita.com/gaogao_9/items/5417d01b4641357900c7

    ↑で紹介しているように,Promise単独ではなくcoやGeneratorと組み合わせて使うのもOKです.async/awaitを使えるようにしてもいいですが,こちらは少しコンパイルの手間が面倒ですね…

    キャンセル

  • 2016/07/28 18:12

    コードを修正してくださりありがとうございました!
    おかげさまで非常に参考になりました。
    自分はいまだに旧石器時代のJavaScriptを書いている原人ですので、これからはできる限り最新のJavascriptの動向に合わせてコーディングしていきたいと思います!
    本当にありがとうございました_(._.)_

    キャンセル

+2

images[i].addEventListener("load",とありますが、これは「ロード時に以下の関数を呼び出す」という非同期処理です。

非同期処理の終了は、setTimeoutなどを使って非同期に検知するしかありません。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.52%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる