ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

外部 script の document.write が何もしない条件などについて

なるほどなー、と思いながら上記記事を読んでた。 記事を読んでて JS の読み込み周りで気になることがあったので調べた。

DOM 操作で追加された script 要素のスクリプトはどのタイミングで実行されるのか?

<!DOCTYPE html>
<html>
(略)
<div id="js-insertion-point"></div>
<script>
  var se = document.createElement("script");
  se.src = "test.js";
  var te = document.getElementById("js-insertion-point");
  te.appendChild(se);
</script>
(略)

上記のような HTML ファイルがあったとして、DOM 操作で追加された src="test.js" の script 要素のスクリプトはどういうタイミングで実行されるのか。 非同期なのはわかるけど、なんらかの実行順序の保証があるのか、全然ないのか。

そういうスクリプティングの話は WHATWG HTML Standards の 4.3 節に書かれてる。

上から読んでいくとわかるのだけど、DOM 操作で script 要素を作った場合、document に挿入された時点 (他のトリガーもあるけど) で prepare the script element の処理が走るらしい。

When a script element that is not marked as being "parser-inserted" experiences one of the events listed in the following list, the user agent must synchronously prepare the script element:

  • The script element gets inserted into a document, at the time the node is inserted according to the DOM, after any other script elements inserted at the same time that are earlier in the Document in tree order.

で、prepare the script element の処理の中を見ていくと、script 要素のフラグによっていろいろ実行のされ方が書かれている。 今回のケースのように DOM 操作で script 要素を作って src 属性で読み込む JS を指定している場合は、最終的に下記に書かれているように実行される。

If the element has a src attribute, does not have an async attribute, and does not have the "force-async" flag set

The element must be added to the end of the list of scripts that will execute in order as soon as possible associated with the Document of the script element at the time the prepare a script algorithm started.

The task that the networking task source places on the task queue once the fetching algorithm has completed must run the following steps:

  • 1. If the element is not now the first element in the list of scripts that will execute in order as soon as possible to which it was added above, then mark the element as ready but abort these steps without executing the script yet.
  • 2. Execution: Execute the script block corresponding to the first script element in this list of scripts that will execute in order as soon as possible.
  • 3. Remove the first element from this list of scripts that will execute in order as soon as possible.
  • 4. If this list of scripts that will execute in order as soon as possible is still not empty and the first entry has already been marked as ready, then jump back to the step labeled execution.
4.3 Scripting — HTML Standard

これを読む限り、「実行できるようになったらすぐに実行する、実行順序に関して何も保証はない」 と読める。 実際に手元の Firefox 20 で確認してみたところ、下記の A. のスクリプト実行は (HTTP レスポンスを返す順番次第で) B. の前にくることもあれば C. の後に来ることもあった。

  • A. DOM 操作で追加された src 属性付きの script 要素のスクリプト実行
  • B. HTML 中に静的に書かれている src 属性付きの script 要素のスクリプト実行
  • C. DOMContentLoaded イベント

ちなみに複数の script 要素を DOM 操作で追加したときも、それらのスクリプトの実行順序には何の保証もなさそう。 まあそれはそうだろうなあという感じではあるけど、静的に HTML を書いているときと同じ感覚で、依存関係にある複数の JS を DOM 操作で読み込もうとするとハマりそう *1 なので注意が必要。

あと、手元の Firefox 20 で試したところ、window の load イベント前に DOM 操作で script 要素を追加した場合は、その script 要素の読み込みが完了するまで load イベントが発生しなかった。 ちょっと意外な感じもするけど仕様通りなのかな。

外部スクリプトの document.write が何もしないのはどういう場合なのか

冒頭の記事では、以下のように書かれている。

これは、"HTML 構文解析器が HTML 文書の末尾まで来たタイミングより後で <script src> で指定されたスクリプト経由で実行されると、document.write は黙って無視される" という仕様によるものです 。(see http://www.whatwg.org/specs/web-apps/current-work/#ignore-destructive-writes-counter )

読み込みのタイミングによっては外部 script のdocument.writeは無視される - HAKOBE blog ♨

『HTML 構文解析器が HTML 文書の末尾まで来たタイミングより後で』 と書かれているけど、HTML Standards を見る限り HTML 文書の末尾までいかなくても、DOM 操作で追加された外部スクリプトからは document.write ができない気がする。

具体的には、下記の条件で document.write の処理は中断されると書かれている。

  • If the insertion point is undefined and either the Document's ignore-opens-during-unload counter is greater than zero or the Document's ignore-destructive-writes counter is greater than zero, abort these steps.
3.2 Elements — HTML Standard

(今は ignore-opens-during-unload counter を無視するとして、) insertion point が未定義で、かつ、ignore-destructive-writes counter が 0 より大きい場合に document.write の処理は中断されるらしい。

で、destructive-writes counter の値が変化するのは、(WHATWG の HTML Standards を読む限りでは) スクリプトの処理時のみ。

  • If the script is from an external file, then increment the ignore-destructive-writes counter of the script element's Document. Let neutralized doc be that Document.
4.3 Scripting — HTML Standard

外部ファイルのスクリプトの場合、HTML 中に静的に書かれている script 要素で読み込まれたものであっても、DOM 操作で追加された script 要素によるものであっても、常に ignore-destructive-writes counter は増加させられるようである。

HTML 中に書かれた script 要素の場合に、外部ファイルのスクリプトからでも document.write が使えるのは、ignore-destructive-writes counter が 0 だからではなく、パーサー側で insertion point の設定がなされるからである模様。

Let the old insertion point have the same value as the current insertion point. Let the insertion point be just before the next input character.

12.2.5 Tree construction — HTML Standard

一方で、DOM 操作で追加された script 要素が実行されるときには insertion point の設定がなされないので、『insertion point が未定義で、かつ、ignore-destructive-writes counter が 0 より大きい』 の条件にあてはまってしまって document.write メソッドが使えなくなる。 多分 『HTML 構文解析器が HTML 文書の末尾まで来たタイミング』 より前でもそうだと思われる。 (Firefox 20 で試したところ、そのような挙動を示した。)

HTML Standards で関係しそうな箇所は大体読んだつもりでここに書いたことはあってると思ってはいるけど、もしかしたら間違ってるかもしれない。

*1:順番に実行すべき複数の JS ファイルがあったとして、DOM 操作で script 要素を作って順番に document に挿入する、ということをしても必ずしもその挿入の順番ではスクリプトは実行されない