innerHTMLを封印してDOMを再帰的に処理する

今までも遊びで何度かdocument内の○○を△△に変換するブックマークレットというのを実装してきましたが、単純なinnerHTMLによる置換ではいくつか問題があって、そもそももの凄く重いということは言わずもがなですが、htmlを一度文字列として扱う必要があるので入れ替える際に、DOMが再構築されて登録されているイベントリスナーごと全て破棄されてしまい、例えばtwitterなどで使用するとページが壊れるという問題がありました。

今回の実装では、それらの問題を解決するために、document.bodyからDOMを再帰的に辿って所定の文字列が含まれるテキストノードだけを処理するようにしています。childNodesプロパティでは、htmlのElementは勿論、テキストノードも並列に取得できるので、意外と簡単に実装することができます。
Node.childNodes – Web API Interfaces | MDN

全てのNodeは、nodeTypeというプロパティを持っていて、elementの場合は1、テキストノードの場合は3が入っています。なので、nodeTypeに1が来たら更にその要素のchildNodesをさらに辿り、それ以外だったらその場で処理させるだけでよいです。
Node.nodeType – Web API Interfaces | MDN

今回のように、dislikeという文字列のテキストノードをhtml要素に変換したい場合は、直接変換することは不可能なので、DOMを生成した後、一旦parentNodeに辿り該当のテキストノードに対してinsertBeforeしたあと、置換前のテキストノードを取り除きます。テキストノードの要素位置を起点としてinsertBeforeできるのは僕は今回はじめて知りました。
dom – Using JavaScript insertBefore() to insert before a TextNode? – Stack Overflow

これらが何に使えるかと言えば、例えば何かの検索結果ページで特定の文字列だけハイライトさせたい場合、一見難しそうにも思えますがdaisuke buttonの実装を流用することで簡単に実装することができます。また、使ってもらうと分かる通り、全てのテキストノードを辿っても意外に実行は早いです。

また、DOMに対する再帰的な処理は、近頃流行している仮想DOMの処理の中でも登場していて、ライブラリによっては、毎回document.bodyから再帰的に処理するため構造が複雑で深いネストをもつdocumentでは、かえって遅くなることもあるようです。

なかなかよい勉強になりました。