これでできる! クロスブラウザJavaScript入門

第8回 実践DOMスクリプティング#1:HTMLとテキストの操作

この記事を読むのに必要な時間:およそ 5 分

innerHTMLとDOM

document.writeの次はやはりinnerHTMLです。こちらも手軽なAPIで,なんといってもクロスブラウザで動作するので頻繁に使用されています。ただし,前回の最後でも触れましたが,innerHTMLはイベントを消してしまうことがありますし,その削除されたイベントが原因でメモリリークを起こすこともあります(メモリリークについては,やはり連載の中盤以降で取り上げたいと思います⁠⁠。パフォーマンスについては使い方を間違えなければ比較的優秀ですが,最善というほどではありません。そのため,innerHTMLを使う際はほかによい方法がないか検討してみるべきでしょう。

さて,1つ典型的なinnerHTMLの誤用例を見てみます。

innerHTMLの誤用例

var data = [1,2,3];
document.body.innerHTML='<ul>'
for(var i = 0;i<data.length;i++){
    document.body.innerHTML += '<li>'+data[i]+'</li>';
}
document.body.innerHTML += '</ul>';

このコードは,<ul></ul><li>1</li><li>2</li><li>3</li>のようなHTMLを作ってしまいます。liがulの中に含まれていません。これはinnerHTMLへの代入のたびにHTMLとして解釈されていることを示しています。HTMLのパースとレンダリングは重い処理なので,パフォーマンスの点でも問題があります。そこで次のように文字列を組み立ててから1度で代入するのが定石です。

文字列とinnerHTMLその1

var data = [1,2,3];
var html_str = '<ul>'
for(var i = 0;i<data.length;i++){
    html_str += '<li>'+data[i]+'</li>';
}
html_str += '</ul>';
document.body.innerHTML = html_str;

ただし,長い文字列を+=で連結するのはIE(特にIE6)で重くなることがあります。そこで,配列を使う方法もよく使われます。

文字列とinnerHTMLその2

var data = [1,2,3];
var html_strs = ['<ul>'];
for(var i = 0;i<data.length;i++){
    html_strs.push('<li>'+data[i]+'</li>');
}
html_strs.push('</ul>');
document.body.innerHTML = html_strs.join('');

なお,このケースではさらに次のように最適化することもできます。

文字列とinnerHTMLその3

var data = [1,2,3];
var len = data.length;
var html_strs = len ? new Array(len) : [];
for(var i = 0;i < len;i++){
    html_strs[i] = '<li>'+data[i]+'</li>';
}
document.body.innerHTML = '<ul>' + html_strs.join('') + '</ul>';

配列の長さが先に決まっているならその長さで配列を初期化しておき,さらにpushではなくカウント用の変数を添字にして代入を行うことで関数呼び出しを減らすことができます。大きな文字列を作る場合にはこういった最適化が大きく影響することもありますが,実際には1msにも満たない差でしかないことのほうが多いでしょう。最適化を行うべきかどうかは悩ましいところですが,1つの目安として「廉価なネットブック上のIE6で遅さを感じるほどでなければ,そもそも最適化の必要はないと判断する」など,何か基準を決めて判断するとよいでしょう。また最適化する場合も,最適化の結果を見ながら可読性やメンテナンスを考えて,落しどころを決めることになります。

HTML中の文字列を置換する方法

ユーザーが検索したキーワードなどをハイライトさせたり,何かの条件にマッチした文字列をマークアップし直したい,置換したいといったケースはよくあることだと思います。

そういったHTML中の文字列を置換するとき,最も手軽で乱暴な方法はdocument.body.innerHTMLの全書き換えです。

文字列とinnerHTML

// BAD!
document.body.innerHTML = 
    document.body.innerHTML.replace(/foo/g,'bar');

この方法は多くの問題を抱えているので,オススメできません。では実際にどういった方法があるのかというと,これが少々厄介です。まず,テキストノードを取得する必要がありますが,クロスブラウザなコードとしては単純に全ノードを再帰的に探索するしかありません。コードとして次のようになります。複雑ではありませんが,HTMLが大きくなるほど処理が重くなってしまいます。

テキストノードの探索

function walker(node) {
  if (node.nodeType === 3) {
    // テキストノードに対する処理
  } else if (node.nodeType === 1 && !/^(IFRAME|STYLE|SCRIPT|TEXTAREA)$/i.test(node.tagName)) {
    var childNodes = node.childNodes;
    for (var i = 0, len = childNodes.length; i < len; ++i) {
      walker(childNodes[i]);
    }
  }
};
walker(document.body);

テキストノードに対する処理,ここでは特定のキーワードにマッチしたとき,その文字列をspanタグで囲ってハイライトするようにしてみます。

テキストノードに対する処理(キーワードハイライト)

if (node.nodeValue.indexOf(keyword) >= 0) {
  var text = node.nodeValue, index;
  var parent = node.parentNode;
  while (text && (index=text.indexOf(keyword)) >= 0 ){
    // テキストを分割し、後ろ側のノードを取得
    var _txt = txt.splitText(index);
    // キーワードの終わりで再度分割
    var __txt = _txt.splitText(keywordLength);
    var s = document.createElement('span');
    s.className ='XXX';
    s.appendChild(_txt);
    parent.insertBefore(s, __txt);
    // ループ用に初期化
    text = __txt.nodeValue;
    node = __txt;
  }
}
実例

ヒット数:

処理時間:

splitTextは指定した位置でテキストノードを分割するメソッドです。DOM Level 1で定義されており,IEでも使えるためクロスブラウザで動きます。少々複雑な処理に見えるかもしれませんが,順を追ってみればテキストノードを2回分割して間のノードをspanタグに詰め込んでいるだけの処理です。

さて,テキストノードの探索はなんとかしたいですね。いくつか方法はありますが,まずIE以外のブラウザで有効なのがXPathです。

テキストノードの探索(XPath)

function xpath(node) {
  var TEXT = 'descendant::text()[contains(self::text(),"' + keyword +
    '") and not(ancestor::'+ ['textarea','script','style','head'].
    join(' or ancestor::') + ')]';
  document.evaluate(TEXT, node, null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
  var len = exp.snapshotLength;
  for (var i = 0; i < len; i++) {
    var txt = exp.snapshotItem(i);
    // テキストノードに対する処理
  };
xpath(document.body);

XPathの場合,XPath自体に検索条件として絞り込みをすることが可能なため,一発で指定のキーワードを含むテキストノードを取得することができます。そのため,非常に高速に動作します。もちろんXPathが万能というわけではなく,今回のようないくつかの条件で絞込したいというケースにマッチしたという面はあります。

さて,IEも何とか高速化したいところです。しかし,IEではネイティブに実装されたXPathは使えません。が,実はIEには少しユニークな方法があります。

IEはTextRangeというテキストノードを操作するためAPIを持っています。このTextRangeのfindTextメソッドでテキストを検索し,さらにexecCommandで文字色や背景色を変えたり,といったことが可能です。

テキストのハイライト(TextRange#findText)

if(document.body.createTextRange){
  var keywordLength = keyword.length;
  var tree = document.body.createTextRange(), prevnode;
  while(tree.findText(keyword)) {
    tree.execCommand('BackColor',false,'#FFFF00');
    tree.move('character',keywordLength);
    // moveしておかないと無限ループになるので注意
  }
}

IEではこれだけで検索からハイライトまで対応できてしまいます。パフォーマンスも十分に高速です。特にマッチする文字列が少ない時に最低限の探索で済むのが特徴です。

実例(最適化)

ヒット数:

処理時間:

まとめ

今回はDOM操作の実例に触れつつ,最適化にも踏み込んで解説しました。次回も引き続きDOM周りの実用的なコードを解説していきたいと思います。

著者プロフィール

太田昌吾(おおたしょうご,ハンドルネーム:os0x)

1983年生まれ。JavaScriptをメインに,HTML/CSSにFlashなどのクライアントサイドを得意とするウェブエンジニア。2009年12月より、Google Chrome ExtensionsのAPI Expertとして活動を開始。

URLhttp://d.hatena.ne.jp/os0x/