【GTM】非同期処理を待機する方法とロジック

catch-img

前回、「【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装」という記事を掲載しました。

記事の最後で「非同期の処理を待つ方法」についてご紹介しました。
あくまで「方法の紹介」だけで、「なぜそうなるのか」というロジックは別記事で、
という風にさせていただいていたので、今回はその内容です。

そのため、「【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装」をご覧になってからの方が
しっくりくると思いますが、そこそこ長い記事なので非同期の部分だけこちらにも再掲します。

つまり本記事は以下のような構成になっています。

  • 前半:非同期を待つ「方法の紹介」(以前の記事の再掲)
  • 後半:なぜ待てるのかというロジックの解説

非同期処理を待つ方法(以前の記事をほぼ再掲)


あくまで『方法の紹介』になり、コードベースのロジックは後述です

さて、GTMは基本的に非同期処理を待つということはしません。

これは前回の記事でいうところの、「カスタムイベントで次のタグを呼び出す」方法でも「タグ配信の優先度」でも「タグの順序付け」を使う方法でも基本的には同じです。

ただ、「タグの順序付け」を使う方法では非同期処理を待つ方法が存在します。
これが「タグの順序付け」を使う一番のメリットといっても過言ではありません。

まずは普通に書いた場合どうなるのかというのを見てみましょう。

通常の「タグの順序付け」


2つのタグを用意します。

■メインタグ – 2秒待機

setTimeout(非同期処理)で2秒待ったあとに、ログを出力します。
setUp(設定タグ)には 後述の 「setUp – 3秒待機」をセットします。

<script>
  (function(){
    var start = window.__start;
   
    setTimeout(function(){
      console.log("%c[メイン - 2秒待機]",
                  "background: red; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 2000);
  })();
</script>

■setUp – 3秒待機

setTimeout(非同期処理)で3秒待ったあとに、ログを出力します。
メインタグの setUp(設定タグ)としてセットされています。

<script>
  (function(){
    var start = window.__start = performance.now();
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 3000);
  })();
</script>


タグ一覧



今までの説明から 「setUp – 3秒待機」が先に動くのは間違いありません。
そして 処理が完了したら成功コールバック = 「メイン – 2秒待機」タグ(がsetTimeoutにラップされた形)で実行されます。

ここの話では 成功コールバックがー・・・とか、setTimeoutでラップされるとー・・・、とか考えると面倒なので、一旦シンプルに 「setUpのタグ」が先に動いて、次に「メインのタグ」が動くと考えてもらって大丈夫です。

我々の期待としては

「setUp – 3秒待機」タグが動く
↓
3秒後にログが出力
↓
と同時に「setUp – 3秒待機」タグの処理が終了
↓
「メイン – 2秒待機」タグが動く
↓
その2秒後(つまり最初から数えると3 + 2 の5秒後)にログが出力

だと思います。

では実際はどうでしょうか。

こうなります。


「メイン – 2秒待機」タグのログが2秒後に出力されてますね。
「setUp – 3秒待機」タグのログは3秒後に出力されています。

これはどういうことでしょうか?

繰り返しですが、「setUp – 3秒待機」のタグが先に動き始めるのは間違いないです。

このタグの中で、処理がsetTimeoutに到着すると
setTimeoutは非同期処理なのでメインスレッドから処理が切り離されます。

そして、setTimeoutは脇においておいて後続の同期処理は普通に終了していましまいます。

同期処理が終わったのでGTMはタグの処理が終了したと判断して
成功コールバック(=「メイン – 2秒待機」)をすぐに呼びます。
(この間は時間にしては一瞬です)

なので「setUp – 3秒待機」と「メイン – 2秒待機」のタグは
ほぼ同時に処理開始することになり、結局2秒後と3秒後にログが出ることになります。

<script>
  (function(){
    var start = window.__start = performance.now();
   
    setTimeout(function(){         // ←非同期処理なのでこれはWebAPIに管理が移る
      console.log("%c[setUp - 3秒待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 3000);
  })();  // ←非同期処理はすっ飛ばして、ここに到着した時点でこのタグは「処理完了」となる
</script>

非同期処理を待つには?

まずは下準備として「変数」の組み込み変数から
「Container ID」と「HTML ID」を有効にしておいてください。



■メイン – 2秒待機 – 非同期待機のコード

こちらはコードの中身は先程と変わりません(わかりやすいようにタグの名前を変えただけです)

<script>
  (function(){
    var start = window.__start;
   
    setTimeout(function(){
      console.log("%c[メイン - 2秒待機 - 非同期待機]",
                  "background: red; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
    }, 2000);
  })();
</script>


■setUp – 3秒待機 – 非同期待機のコード

<script>
  (function(){
    var start = window.__start = performance.now();
     
    var gtm = window.google_tag_manager[{{Container ID}}];
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機 - 非同期待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
 
      gtm.onHtmlSuccess({{HTML ID}});  // ★!! ここが重要 !!★
 
    }, 3000);
  })();
</script>

何をしているかというと、google_tag_managerオブジェクトから
自身のコンテナIDのオブジェクトを取得して、onHtmlSuccessメソッドを呼んでいます。

onHtmlSuccessメソッドを手動で呼ぶ記述をしている場合
GTMはこの onHtmlSuccessが呼ばれるまで次のタグの処理を待機
します。

今までの言葉でいいますと onHtmlSuccessを呼ぶことで
「成功コールバック(=「メイン – 2秒待機 – 非同期待機」をsetTimeoutでラップした関数)」
を手動で呼び出すということをしています。

※手動で呼び出す前に、同期処理が終わった段階で
自動の成功コールバックが呼ばれるんじゃない?と思いますが
それは内部でうまいこと処理しています。(このあたりを後半の内容で)

結果、以下のようなログ出力になります。


3秒後に「setUp – 3秒待機 – 非同期待機」タグのログが出力されて
そこまでの処理が終わってから(setTimeoutで待つ処理も終わってから)
「メイン – 2秒待機 – 非同期待機」タグの処理が開始されます。

そしてさらに2秒後(最初から数えると 3 + 2 = 5秒後)に
「メイン – 2秒待機 – 非同期待機」タグのログが出力されます。

不思議なところ – 疑問編

非同期処理を待つ方法は上記の通りですが
不思議なところ(なんでこうなるんだ)という点はどこでしょうか?

もちろん人によって様々だと思いますが1つあるのは上記でも記載した
「自動で呼ばれる成功コールバックはどこいった?」という点です。

前回(「【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装」)の記事で
GTM内のほぼすべてのタグは同期処理が終わったら「成功コールバック」を自動で呼ぶという話をしました。

また、タグの順序付け設定をした場合は
setUpタグの成功コールバック = メインタグ(をsetTimeoutでラップした関数)になっている
という話もしました。

そのため、setUpタグ実行 → setUpタグの同期処理終了 → 自動で成功コールバックが発火 → メインタグ実行

となり、同期処理をしている限り、setUpタグ→メインタグの順序が保証されるわけです。

では非同期処理が絡んだら?

コード内に非同期処理があったとしても、同期的処理が終わった時点で、自動で成功コールバック(メインタグ)が呼ばれるはずでは?

例として、以下のようなコード(setUpタグ)があるとします。

<script>
    // ①    
    var gtm = window.google_tag_manager[{{Container ID}}];
 
    // ②
    setTimeout(function(){
      console.log("ログ");
      gtm.onHtmlSuccess({{HTML ID}});  // ④
    }, 3000);
    
   // ③
</script>

JavaScriptは上から実行されますので
①→②→③ と処理しますが、この時点ではまだ④は実行されていません(実行されるのは3秒後)。
しかし、③の時点で「同期処理」は終了しています。

なので、③の時点で自動の成功コールバックが呼ばれ、次のタグ(メインタグをsetTimeoutでラップした関数)が実行されてもおかしくないわけです。

実際、途中でonHtmlSuccessを手動で呼んでなければ、そうなります(同期処理が完了した時点で自動の成功コールバックが呼ばれます)

でも、非同期処理を待つ実装をしている場合(onHtmlSuccessを手動実装している場合)は、そうならずに④まで待ってから成功コールバックが呼ばれているように見えるわけですね。

コード内に非同期の文脈があることを、GTMはコード実行前に把握出来ているのでしょうか??

あるいは、GTMはコード実行前に「このコードには `gtm.onHtmlSuccess`が書かれているから、自動の成功コールバックの実行はキャンセルしよう」という判定ができるのでしょうか???

みたいなことが不思議だと思うわけです。

そのあたりを解き明かしていきましょう。

非同期処理を待つロジック

ここからはロジックの紹介です。
コードもバリバリ出てきますので、どちらかというとエンジニア向けです。

なお、gtm.jsの変数名や関数名はコンテナやタイミングでビルドしたときに命名が変わりますので
以下出てくる関数名は筆者の今の手元の環境ではそうなっていた、という形になります。

前回の記事では見やすいようにリライトしていましたが
今回はそのまま書いていってしまおうと思います。

GTMのタグが実行されるところから話していきましょう。

サーバーでのビルド

例えば、GTMで以下のようなカスタムHTMLタグを登録していたとします。

想定としては、メインタグのsetUpタグとして 登録しているとしてください。

<script>
  (function(){
    var start = window.__start = performance.now();
     
    var gtm = window.google_tag_manager[{{Container ID}}];
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機 - 非同期待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
      gtm.onHtmlSuccess({{HTML ID}});
    }, 3000);
  })();
</script>


さて、GTMが設置してあるページが読み込まれると
gtm.js ちょーだいとリクエストが投げられます。

これをやっているのがGTM本体のこのコードですね。

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->

GTMの管理画面からコピペするアレですね。


通常は、「https://www.googletagmanager.com/gtm.js?id=GTM=XXXXXX」のようなURLへリクエストを投げます。

GTG(GoogleTagGateway)を設定していたり、sGTM(SeversideGTM)でウェブコンテナのクライアントを設定している場合は、自社ドメインになっているケースもあると思います。(このあたりもまた別記事で)

ともあれ、リクエストのレスポンスとして、GTMサーバーから配信される gtm.js は
GTM内部の設定を反映した形で配信されます。

特に重要なことは、単純にタグの中身を文字列として返すのではなく
「文字列」と「マクロ(変数)呼び出し命令」が混ざった『配列』(vtp_html)として配信されることです。

例えば一番最初に書いたコード(以下)

<script>
  (function(){
    var start = window.__start = performance.now();
     
    var gtm = window.google_tag_manager[{{Container ID}}];
   
    setTimeout(function(){
      console.log("%c[setUp - 3秒待機 - 非同期待機]",
                  "background: blue; color: white;",
                  ((performance.now() - start)/1000).toFixed(1) + "秒経過");
      gtm.onHtmlSuccess({{HTML ID}});
    }, 3000);
  })();
</script>

は次のような形で、gtm.js内では記されます。

"vtp_html": [
  "template",
  "<script type=\"text/gtmscript\">(function(){var a=window.__start=performance.now(),b=window.google_tag_manager[",
  ["escape", ["macro", 1], 8, 16],
  "];setTimeout(function(){console.log(\"%c[setUp - 3秒待機 - 非同期待機]\",\"background: blue; color: white;\",((performance.now()-a)/1E3).toFixed(1)+\"秒経過\");b.onHtmlSuccess(",
  ["escape", ["macro", 2], 8, 16],
  ")},3000)})();</script>"
]

分かりますでしょうか。

「ただの文字列」の部分と「GTM変数」の部分で区切られ、それが「配列」として保持されています。
上記例ですと「[“escape”, [“macro”, 1], 8, 16]」が {{Container ID}} を呼び出す命令になっています。

indexの最初が「template」になっていることも注目です。

vtp_htmlの評価


トリガー条件を満たし、タグの実行準備が始まると、GTMの変数展開エンジンが
vtp_html の配列を再帰的に評価していきます。

変数展開がどのように行われているかというと、一部抜粋すると

case "template":
        d = [];
        for (var p = !1, q = 1; q < a.length; q++) {
          var r = ch(a[q], b, c);         // 各パーツを再帰評価
          Vg && (p = p || Vg.vs(r));      
          d.push(r);
        }
        return Vg && p ? Vg.vr(d) : d.join("");

のような switch-case 文で評価されます。
ch関数で各配列パーツを順次評価していきます。

さて、この ch 関数の戻り値が面白いところです。

普通の変数であれば、r (=chの戻り値)はただの文字列です。

しかし、{{HTML ID}} 変数の場合は特殊なオブジェクト(目印)を返します。
ここではその特殊なオブジェクトを「PHオブジェクト」と命名し、以下この名称を使っていきます。

つまり、{{HTML ID}} 変数の場合、「rは PHオブジェクト」になります。

そしてこの部分

Vg && (p = p || Vg.vs(r));  

Vg.vs(r) は 「r(=chの戻り値) が PH オブジェクト かどうか」を判定する関数です。
PHオブジェクトであれば true を返します。
つまり、カスタムHTMLの中に {{HTML ID}} が使われていれば、p が true になります。

そして最後の return 部分

return Vg && p ? Vg.vr(d) : d.join("");

p が true (= {{HTML ID}} が含まれていた) なら Vg.vr(d)を return し
p が false (= 普通の変数しかなかった) なら、ただの文字列として結合(d.join(“”))して return します。

ここで、Vg.vr(d) は これまた特殊なオブジェクトを作る関数だと思ってください
Vg.vr(d)で返されるオブジェクトを 「HIDオブジェクト」と命名します。
(つまり、カスタムHTML内に {{HTML ID}} が使われていれば、HIDオブジェクトが return されます)

case文の箇所を改めてコメントも含めて記載すると以下のような感じです。

// 展開エンジン内の "template" 処理部分
case "template":
  d = []; // 結合前のパーツを格納する配列
  for (var p = !1, q = 1; q < a.length; q++) {
    // 変数(マクロ)や文字列を評価して r に代入
    // {{HTML ID}} が使われている場合は PHオブジェクトを返し
    // それ以外の変数であれば 文字列 を返す
    var r = ch(a[q], b, c);
     
    // ▼ プレースホルダーの検知 ▼
    // Vg.vs(r) は「r が PH(特殊プレースホルダー) かどうか」を判定する関数。
    // 評価結果の中に {{HTML ID}} が混ざっていれば p が true になる。
    Vg && (p = p || Vg.vs(r)); 
     
    d.push(r);
  }
   
  // ▼ 最終的なリターン部分 ▼
  // p が true (= {{HTML ID}} が含まれていた) なら Vg.vr(d) つまり HIDオブジェクトとしてreturn。
  // p が false (= 普通の変数しかなかった) なら、ただの文字列として結合(d.join(""))する。
  return Vg && p ? Vg.vr(d) : d.join("");

つまり、今回の例でいいますと、gtm.js 内で

"vtp_html": [
  "template",
  "<script type=\"text/gtmscript\">(function(){var a=window.__start=performance.now(),b=window.google_tag_manager[",
  ["escape", ["macro", 1], 8, 16],
  "];setTimeout(function(){console.log(\"%c[setUp - 3秒待機 - 非同期待機]\",\"background: blue; color: white;\",((performance.now()-a)/1E3).toFixed(1)+\"秒経過\");b.onHtmlSuccess(",
  ["escape", ["macro", 2], 8, 16],
  ")},3000)})();</script>"
]

と定義された vtp_html があり、
この中身が1つずつ ch関数で評価されます。

最初は

"<script type=\"text/gtmscript\">(function(){var a=window.__start=performance.now(),b=window.google_tag_manager["

2番目に

["escape", ["macro", 1], 8, 16]

3番目に

];setTimeout(function(){console.log(\"%c[setUp - 3秒待機 - 非同期待機]\",\"background: blue; color: white;\",((performance.now()-a)/1E3).toFixed(1)+\"秒経過\");b.onHtmlSuccess("

4番目に

["escape", ["macro", 2], 8, 16]

最後に

")},3000)})();</script>"

ですね。

1個ずつ評価していって、評価結果を d という配列に push しているわけです。

で、この例ですと4番目の [“macro”, 2] が {{HTML ID}} を指しますのでこの時点で

// Vg.vs(r) は「r が PH(特殊プレースホルダー) かどうか」を判定する関数。
// 評価結果の中に {{HTML ID}} が混ざっていれば p が true になる。
Vg && (p = p || Vg.vs(r)); 

Vg.vs(r) が true になり、 p に true が代入されるわけです。(p = true)

そして先程見たように p が true なら Vg.vr(d) として HIDオブジェクトを return します。
p が false({{HTML ID}} が使われていない)場合は、ただの文字列結合 (d.join(“”)) が行われ return されます。

// ▼ 最終的なリターン部分 ▼
// p が true (= {{HTML ID}} が含まれていた) なら Vg.vr(d) つまり HIDオブジェクトとしてreturn。
// p が false (= 普通の変数しかなかった) なら、ただの文字列として結合(d.join(""))して return。
return Vg && p ? Vg.vr(d) : d.join("");

DOM挿入時の動き

これは、前回の記事(「【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装」)の
『フェーズ3:DOM挿入・展開』にあたる部分です。

「タグの順序付け」においては、面白いところはこのフェーズより前に終わっているのでさらっと流したのですが、今回(非同期処理の待機)で一番面白いのはここからです。

面白いところの説明に行く前に、この時点で何が行われているのか簡単に再掲したいと思います。
詳しくは【GTM】タグ配信の優先度 / タグの順序付け と 非同期処理を待つ実装」の記事をご覧ください。

関数名なども上記記事から持ってきている(本記事では1度も出ていなかったりするので)、しっかり把握されたい方は上記記事を見てくださいませ。

前提として改めてですが、「メインタグ」の setUpとして「setUpタグ」が設定されいるという状況。
そして、setUpで 非同期を待つ処理を記述しているということ。

  1. fireTagsForEvent が呼ばれる
  2. buildExecuteClosure が呼ばれる。
  3. createTagExecutionFunction が呼ばれる。
  4. createTagExecutionFunction の中で executeFunction が定義され、これが返却される (= executeThisTag)。(※定義されただけで、実行はされていない)
  5. buildExecuteClosure の中で setUp があった場合、buildExecuteClosure を再帰的に呼んで、戻り値を上書きする。
  6. 最終的に一番最初の executeFunction(= executeThisTag) が buildExecuteClosure の戻り値として、fireTagsForEvent に返却される。
  7. fireTagsForEvent で execute() される。つまり、返却された一番最初の executeFunction(= executeThisTag) が実行される。(setUpタグの実行が走る)
  8. executeFunction(= executeThisTag) が実行されると「この瞬間」に evaluateMacros が走り、最新のデータが取得される。
  9. tagPayload.vtp_gtmOnSuccess に「次のタグ(nextTagToRun)」をキックする関数が定義される。
  10. dispatchToSandbox が呼ばれる。
  11. dispatchToSandbox の中で vtp_gtmOnSuccess が gtmOnSuccess にリマップ(変換)される。
  12. sandboxDispatcher が呼ばれる。
  13. wrapCallbacksWithSetTimeout が呼ばれる。
  14. その中で deferToMacroTask が使われ、gtmOnSuccess が setTimeout(..., 0) を呼ぶように上書きされる。
  15. 実際にDOMに挿入されることで、setUpタグに記述されていたコードが同期的に実行される。
  16. 同期処理が完了すると、GTM内部から gtmOnSuccess が呼ばれるが、中身は setTimeout なのでタスクキュー(マクロタスク)に積まれる。
  17. コールスタック(GTMの現在の処理や dataLayer.push の消化)がすべて空になり、タスクキューの出番が来たら、次のタグ(メインタグ)が安全に実行される。

タグ実行の順序としてはDOM挿入前までで、上記の「14」まで終えている状況で
これから「15」のフェーズに進もうというところです。

ざっくりと日本語でも状況を説明すると

メインタグが評価される

↓

setUp設定があるのでsetUpタグが評価される

↓

setUpタグの「成功コールバック関数」が「メインタグの実行関数そのもの」になる

↓

「成功コールバック関数(=メインタグの実行関数)」が setTimeoutでラップされる(マクロタスクになる)

ということが行われるのでした。

このまま実行されると、setUpタグ実行終了時に『成功コールバック関数(=メインタグの実行関数)を setTImeoutでラップしたもの」が呼ばれ、マクロタスクとしてタスクキューに積まれるのでした。

結果として、処理が同期的である限り、setUpタグ実行→メインタグ実行という順序が保証されるのでした。

(一方で、コールスタックが空になってからでないとタスクキューに積まれた関数の実行は行われないため、思っているよりも 成功コールバック(メインタグ)の実行は遅くなり得るという話もしました)

というわけで、このまま実行されると上記のようになるのですが
非同期処理の待つ場合は、実行される前、「DOM挿入時」に面白いことがあるという話です。

さて、DOM挿入直前に何が行われるかというと、以下の関数が実行されます。

// DOM挿入直前:コールバックを準備・すり替える関数
var EE = function (a, b, c) {
    // a: 展開エンジンから渡されたHTMLコンテンツ (文字列 or HIDオブジェクト)
    // b: DOM挿入直後にGTMが自動で呼ぶはずの「成功コールバック(onSuccess)」
 
    // a が HIDオブジェクトなら、{{HTML ID}} が使われていた証拠!
    a instanceof CE && ((a = a.resolve(DE.O(b, c))), (b = yb));
     
    return {
        hs: a,         // 実体化されたHTML文字列
        onSuccess: b   // (すり替え済みの)コールバック(何もしない関数)
    };
};
 
// 何もしない空の関数
function yb() {} 

まず、

a instanceof CE && ((a = a.resolve(DE.O(b, c))), (b = yb));

aというのが展開エンジンから渡されたコンテンツだと思ってください。
つまり、先ほど以下で return されたものです。
今回は {{HTML ID}} を使っている前提なので、HIDオブジェクトが a に入っています。

// ▼ 最終的なリターン部分 ▼
// p が true (= {{HTML ID}} が含まれていた) なら Vg.vr(d) つまり HIDオブジェクトとしてreturn。
// p が false (= 普通の変数しかなかった) なら、ただの文字列として結合(d.join(""))して return。
return Vg && p ? Vg.vr(d) : d.join("");

b は 成功コールバック(=メインタグを setTImeoutでラップした関数)です。
(先程の説明の通り、この時点で setTimeoutにラップされています。)

a instance CE は a が HIDオブジェクト ({{HTML ID}} が使われた) かどうか、を判定しています。
HIDオブジェクトであれば、&& の右側が評価されて、((a = a.resolve(DE.O(b, c))), (b = yb)) が実行されます。

a = a.resolve(DE.O(b, c)))

これは、b (成功コールバック(=メインタグを setTImeoutでラップした関数)) を
GTM内部の別の場所にコピーして保持する
、ということをしています。

そして戻り値は 文字列になります。
この直前まで a は HIDオブジェクトだったわけですが
a.resolve() を呼ぶことで、最終的にピュアな HTML文字列として上書きされます。

さらに、

// ybは何もしない空の関数
b = yb

ここで、b を 何もしない関数で上書きしています。

そして

return {
    hs: a,         // 実体化されたHTML文字列
    onSuccess: b   // (すり替え済みの)コールバック(何もしない関数)
};

return として ピュアなHTML文字列(この時点で特殊オブジェクト(HIDオブジェクト)ではなくなり、文字列になっている)と、成功コールバックとして b ( = 何もしない空の関数) が返されるわけです。

つまり、何をしているか?

重要なのは、a が HIDオブジェクト(カスタムHTML内で {{HTML ID}} が使われている)の場合
本物の成功コールバック(=メインタグを setTimeoutでラップした関数)は別の場所に退避させ、
偽りの成功コールバックとして「何もしない空の関数」を渡しているということです。

DOMへの展開と空振りする自動コールバック

EE関数(上記関数)を通過した、完成品の HTML文字列は、実際のページに <script>タグとして挿入されます。

DOMへの挿入が完了し、同期処理が終わると、GTMは自動で「成功コールバック」を呼び出そうとします。

ところがどっこい
この時点で 成功コールバックは「何もしない空の関数」にすり替えられているため、ここでは何も実行されません。

手動コールバックの実行と次タグへの連鎖


そしてDOMに挿入されたコードの非同期部分の実行が終わると、gtm.onHtmlSuccess()が呼ばれるわけです。

setTimeout(function(){
  // 3秒後に手動でコールバックを呼び出す
  gtm.onHtmlSuccess({{HTML ID}});
}, 3000);

上記例だと3秒後に、手動で gtm.onHtmlSuccess()が呼ばれると、
このメソッドは先程 退避させた場所から 「本物の成功コールバック(=メインタグを setTimeoutでラップした関数)」を呼び出すという流れになります。

よって、3秒待ってから、次の関数(成功コールバック = メインタグをsetTImeoutでラップした関数)が実行され、
タスクキューに積まれて、コールスタックが空になったら実行される、というフローをたどります。

これで、非同期処理を待機する流れは以上です。

不思議なこと – 解決編

疑問編で提示したことは解決したでしょうか?

「非同期処理を待つ」というのは
具体的にはカスタムHTML内で {{HTML ID}} という変数が使われているかどうかが重要になるわけですね。

{{HTML ID}} が使われていたら、特別なオブジェクトを返して
DOMに挿入する際にそのオブジェクトがあれば 成功コールバックを「空の関数」にすり替えるわけです。
同時に本物の成功コールバック(=メインタグをsetTimeoutでラップした関数)は別の場所の退避させます。

setUpタグのカスタムHTML内の同期的処理が終わった段階で
自動で成功コールバックが呼ばれるわけですが
空の関数にすり替えられているので実行されても何も起きません。

そして、非同期の処理が終わって手動で gtm.onHtmlSuccess() が呼ばれた際に
退避させておいた本物の成功コールバック(=メインタグをsetTimeoutでラップした関数)を実行して
次の処理に進むことになります。

疑問編への解答のポイントとしては、GTM内部ではカスタムHTMLを実行する前から
コード内で {{HTML ID}} が使われていることを判定できている
ということです。

疑問編でも出した例ですが、以下のような JavaScriptが メインタグの setUpタグとして設定されていたとします。

<script>
    // ①    
    var gtm = window.google_tag_manager[{{Container ID}}];
 
    // ②
    setTimeout(function(){
      console.log("ログ");
      gtm.onHtmlSuccess({{HTML ID}});  // ④
    }, 3000);
    
   // ③
</script>

JavaScriptのコードを基本的には上から順に実行していきますね。(①→②→③)
そして、setTimeout に来た時点でそこの処理は一旦WebAPIに投げて、タスクキューに積まれます。

通常は(GTMではなく例えばハードコードされていたら)、3秒後に gtm.onHtmlSuccess({{HTML ID}}) (④)が実行されて、初めてgtm.onHtmlSuccess({{HTML ID}}) が書かれていたんだな、と分かるわけです。
つまり、コード実行前の時点で「このコードの中には {{HTML ID}} の記述があるぞ!」とは知る由もなく、実行されるまで分かりません。

もちろん、このスクリプトをすべて文字列化して、事前に indexOf("{{HTML ID}}") などで検索すれば
「このコードには {{HTML ID}} が含まれている」みたいなことは一応分かると思います。

これは文字列化して探しているという例ですが
GTM内部では、文字列化して indexOf するとかではないわけですね。

もっと賢い、かつ複雑な方法で事前にコード内で {{HTML ID}} が使われていることを検知して
使われていた場合は、自動で呼ばれる成功コールバックを何もしない関数にすり替える。
そして、手動で呼ばれたときに本物の成功コールバックを実行する。という仕組みを成立させている
わけです。

事前に検知する必要があるのはなぜでしょう?
疑問編でも書きましたが、事前に検知できなければ、同期処理が終わった時点で自動成功コールバックが呼ばれてしまいます。

そこで、事前に {{HTML ID}} があることを検知して
{{HTML ID}} が使われているから、自動の成功コールバックは空の関数で空振りさせて
手動で成功コールバックが呼ばれるまで待とう
、となるわけです。


まとめ

いかがでしたでしょうか。

GTMで非同期処理を待つ方法自体があまり認知されていないかもしれませんので
今回は「方法」の紹介と
主にエンジニア向けにどういう「ロジック」でそうなっているかを簡易的に紹介させていただきました。

GTMで非同期の処理を待ってから、次の処理へ進みたいというケースがある場合は
この記事を思い出していただければ幸いです。

以上になります!


Tomonaga
Tomonaga

人気記事ランキング

タグ一覧