GANCHIKU.com

JavaScriptで重い処理をするとアニメーションGIFが止まる件について

2007年9月30日

当分の間JavaScriptオンリーです。今日はですます調です。

Ajaxのローディングとかでクルクル回るアニメーションGIFてありますよね?あのローディングのアニメーションGIFをJavaScriptで重い処理をするときに使おうと思っていてその画像を探していました。そして、先日調べていたら、こんなサイトを発見しました。激しくいいですね。
Ajaxload – Ajax loading gif generator
実は、結構有名なサイトなんですね。知らなかったです。でも、先日知って、早速作ってみました。
ローディング画像

そして、早速使おうと思ったのですが、重い処理をしている間にアニメーションGIFを表示させていてもアニメーションがされないのです。。。つまり、アニメーションではない状態のGIF画像が表示されるだけなのです。その原因はJavaScriptが処理されている間だからです。また、重いJavaScriptが処理中の際にはその間はブラウザが固まります。

その解決方法を探していたのですが、その際にようやく私もsetTimeoutの使いかたがわかりました。そして、ページ描画のタイミングを制御できるようになりました。そうです。ポイントはsetTimeoutです。重い処理を一つの関数に入れちゃダメなんですね。つまり、これはダメなのです。

    Event.observe(window, 'load', function() {
        var loadingImage = Builder.node('img', {src: 'loading.gif'});
        var sync = $('sync');
        var async = $('async');
        var loading = $('loading');
        var working = $('working');
        var loop = $('loop');
        Event.observe(sync, 'click', function() {
          var breakNumber = 500;
          var callback = function() {
            // ここが重いということにする。例えば、以下はまぁまぁ重い。
            while (breakNumber--) {
              working.appendChild(Builder.node('div', {className: 'test'}));
              document.getElementsByClassName('test');
            }
            loading.removeChild(loading.firstChild);
            loop.innerHTML = 501 - breakNumber;
            sync.disabled = false;
          }
          loading.appendChild(Builder.node('div', [loadingImage, "Loading..."]));
          sync.disabled = true;
          setTimeout(callback, 0);
        });
     });

あ。ちなみにprototype.jsとscript.aculo.usがあることを前提に書いているので、その辺は適当にほげほげしてください。まぁ、ここではsetTimeoutを使用して、その中で重い処理を実行していますね。処理を始める前にアニメーション画像を出して、処理が終わる際にローディングの画像を消す処理をしています。そして、while文の中を勝手に重い処理としてここでは使用しています。しかし、このwhile文が流れている間、いや正確にはcallback関数が実行されている間はアニメーションGIFが止まります。

そこで、アニメーションGIFを止めないような方法を考えました。その根本にある考えは。。。重い処理は一つの関数に入れないということです。つまり、一つ関数の中にループで重い処理を実行させるのではなく、再帰を使用して何個も関数を呼び出す必要があります。もちろんそれが再帰処理ができないような重い処理であればこの方法は無理ですが、往々にして重い処理というのはDOMを使ってイテレーションを使用するところになる可能性が高いと思いますので、それを再帰処理に置き換えましょう。そして、修正したコードは以下の通りです。

      Event.observe(window, 'load', function() {
        var loadingImage = Builder.node('img', {src: 'loading.gif'});
        var sync = $('sync');
        var async = $('async');
        var loading = $('loading');
        var working = $('working');
        var loop = $('loop');
        Event.observe(async, 'click', function() {
          var breakNumber = 500;
          var callback = function() {
            if (breakNumber--) {
              working.appendChild(Builder.node('div', {className: 'test'}));
              document.getElementsByClassName('test');
              setTimeout(callback, 0);
            } else {
              loading.removeChild(loading.firstChild);
              async.disabled = false;
            }
            loop.innerHTML = 501 - breakNumber;
          };
          loading.appendChild(Builder.node('div', [loadingImage,"Loading..." ]));
          async.disabled = true;
          setTimeout(callback,0);
       });
      });

これで、重いループををしていても、だいたいの場合はアニメーションGIFが止まらずに済みます。「だいたいの場合」と書いたのは理由があります。実は、私の実環境では、もっと重い処理をしなければいけない用件がありましたので、修正版でもアニメーションGIFが動きませんでした。正確にはfirefoxでは大丈夫でした(たぶんそれほど重くなかったのが原因かもしれません。)が、IE6ではアニメーションGIFが止まりました。原因は、再帰の中で呼び出しているsetTimeoutのmillisecondの値でした。私はどうせ次の処理を実行してもらうのだから0でいいのではないか、と考えていたのですが、0だとすぐ関数をエンキューするのはいいのですが、現在走っている処理が終わると、すぐキューにたまった関数をデキューして実行するので、JavaScriptの処理が常に動いていることになったようです。あくまで想像ですが。なので、ここにラグを与える必要があったのです。そこで、setTimeoutに渡すmillisecondの値を適当な数値に変更すると、カクカクするけど、アニメーションGIFが動いてくれました。
動くサンプルは以下に置いておきます。ちなみにこのサンプルはカクカクしません。
Animation GIF and JavaScript setTimeout

404 Blog Not Found:javascript – ページはいつ再描画されるかが激しく参考になりました。

先日、jQuery開発者向けメモを見て、一通り試してみましたがなかなか直感的に書けそうなライブラリなので今後使用することを検討してみます。今、開発の勢いが一番あるライブラリなので追いかけたりするのが楽しそうですね。prototype.jsとscript.aculo.usは、なかなか新たなリリースが無く、ちょっとさみしいですが、これはこれでライブラリとして成熟段階に入ったといことでアリなのでしょうね。

さて、本日はTOEICを受けてきました。過去に2回ほど受けたのですが、相変わらず時間が足らなかったです。問題が中途半端に全問解けそうなので、ついついちゃんと読んでやってしまうのが原因です。そのため最後に15問ほど残ってしまいます。スピード勝負はつらいですね。でもまぁ、おそらくそれなりのスコアは出たでしょう。試験地の愛知大学が思ったよりもとてもキレイでした。

先日激しくヘコむことがありましたが、ようやくすっきりしました。今日から強く生きていこうと思います。

DOM検索効率化のメソッド群を作ってみた。

というわけで、もういっちょ。考えていたDOM検索効率を考えたメソッド群を作ってみた。

ええと、prototype.jsのElementオブジェクトにaddMethodsしていることからもわかるようにprototype.js必須っす。ええと、私の使っているprototype.jsは、1.5.1ね。script.aculo.us(v1.7.1_beta3)と一緒に使っているので。

document.getElementsByClassNameAndTagName = function(className, parentElement, tagName) {
  if (Prototype.BrowserFeatures.XPath) {
     return $(parentElement).getElementsByClassName(className);
  } else {
    var children = $(parentElement).getElementsByTagName(tagName || '*');
    var elements = [], child;
    for (var i = 0, length = children.length; i < length; i++) {
      child = children[i];
      if (Element.hasClassName(child, className))
        elements.push(Element.extend(child));
    }
    return elements;
  }
}

Element.addMethods({
  getElementsByClassNameAndTagName: function(element, className, tagName) {
    return document.getElementsByClassNameAndTagName(className, element, tagName);
  },

  nextElement: function(element) {
    do {
      element = element.nextSibling;
    } while (element && element.nodeType != 1);
    return $(element);
  },

  previousElement: function(element) {
    do {
      element = element.previousSibling;
    } while (element && element.nodeType != 1);
    return $(element);
  },

  firstChildElement: function(element) {
    var child = element.firstChild;
    while (child && child.nodeType != 1) {
      child = child.nextSibling;
    }
    return $(child);
  },

  lastChildElement: function(element) {
    var child = element.lastChild;
    while (child && child.nodeType != 1) {
      child = child.previousSibling;
    }
    return $(child);
  },

  childElements: function(element) {
    var children = [];
    var child = element.firstChild;
    while (child) {
      if (child.nodeType == 1) {
        children.push($(child));
      }
      child = child.nextSibling;
    }
    return children;
  },

  childElement: function(element, index) {
    var nodeIndex = 0;
    var child = element.firstChild;
    while (child) {
      if (child.nodeType == 1 && index == nodeIndex++) {
          return $(child);
      }
      child = child.nextSibling;
    }
    return null;
  },

  cleanWhitespaceRecursive: function(element) {
    var f = function(element) {
      var child = $(element).cleanWhitespace().firstChild;
      while (child) {
        if (child.nodeType == 1) {
          f(child);
        }
        child = child.nextSibling;
      }
    };
    f(element);
    return element;
  }
});

nextElement, previousElementはそのまんま。textNodeはすっ飛ばして、elementNodeだけを見ている。firstChildElementとlastChildElementはfirstElementとlastElementって命名しようかと思ったけど、childだということを意識したかったのでちょいと冗長だけど、これで堪忍してや。で、意味もそのまんま。textNodeをすっ飛ばして最初や最後のelementNodeを返す。

childElementsは、前回のポストの結果を考慮してfirstChildとnextSiblingを採用。elementNodeの配列を返す。childElementは、index指定でelementNodeを返す。本当は、こんな感じでchildElementsメソッドの返す配列のindexを返す方がすっきりしていていいんだけどなぁ。

  childElement: function(element, index) {
     return $(element).childElements()[index];
  },

しかし、それだとnextSiblingを全て見てまわってしまうので、遅くなりそうなので、不採用。

cleanWhitespaceRecursiveはついでの産物。今回作成したものとは関係がないのだけども、一応。HTMLコーダにじかにHTMLを書かれるとwhitespaceが入ってしまい、困るので再帰的に消してみることにした。JavaScriptで再帰をするときってやっぱり、ローカル変数に関数をぶちこんで、それを何度も呼び出す方がいいのかな、と思ったので、自分自身を何度も呼び出すような方は不採用。
自分自身を呼び出すのは、こんな感じか。

  cleanWhitespaceRecursive: function(element) {
    element = $(element);
    var child = element.cleanWhitespace().fistChild;
    while (child) {
      if (child.nodeType == 1) {
        child.cleanWhitespaceRecursive();
      }
      child = child.nextSibling;
    }
    return element;
  }

どっちがいいんだろうなぁ。。。

で、本当は、オプションで、$指定で返すか、そのままのelementを返すかを指定できるようにするかで迷ったのだけど、prototype.jsと一緒に使うことを前提としているので、$で返すようにした。これもパフォーマンスに関係してくるんだけど、まぁ、その辺は考慮中。

つーか、また後で追記したり、ソース修正するかもしれん。名前はなんでもよかったのだけど、domx.jsとしよう。

< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

  
    
    
    
    
    
    
    

  
  

ソースはここ。

domx.js
ライセンスは適当っす。自己責任で使ってちょ。

で、上のソースを動かす形にしたデモもここに置いておく。domx demo

うーん。そろそろjQuery使ってみようかなぁ。。。onReadyとか便利そうだし。。。インデントがタブなのが非常に嫌なので、敬遠しているのだけど。。。

全然関係ないが、ここ二日ほど愛知県図書館で開発をしている。なかなか良いね。ホットスポットもあるみたいだけど、契約をしていないのでインターネットにつなげない。でも、そのおかげで効率がいいよん。インターネットがあるとダラダラしちゃって、ダミだ。

DOM検索効率化をもう少し追ってみる。

やったよ。やった。ようやくキーボードが届いたよ。ここ3ヶ月、私のキーボードはC-xと]}のキーが壊れていたのだけど、ようやく快適に打てるようになったよ。私の状態を見兼ねて買ってくれた学生時代の恩師の稲葉先生どうもありがとうございます。つーか、そのくらい自分で買えばいいのだけど、なんか買う気がなかなか起こらなくて、ダラダラしてました。。。

で、さっそく簡単なプログラムを書きはじめている。えと、実はまだDOM検索周りを調べているのだけど、一つ疑問があがってきた。それは以下の通り。

childNodesでの検索とfirstChildとnextSiblingの検索について。つまり、どちらでも子ノードを取得することができるのだけど、実際どっちを使ったらいいの?

ググってみるとパフォーマンスを見ている人がいたのだが、childNodesよりも、fistChildを取ってnextSiblingで検索する方が速いということらしい。特に、IE6が良くないとのこと。ブラウザの実装によるのね。。。
new Blah().list(); node.childNodes[] performance
うーむ。てっきりnextSiblingって、いかにもリンクリストな感じでその要素の数を線形探索しないといけないから、O(log(n))で、childNodesでindex指定で取得したら、O(1)で速いんだろうなぁ、なんて思っていたのだけど、どうやらそうでないみたい。。。と鵜呑みにするのもどうか、と思うのだが、まぁ、いいや。

ええと、何でこんなことを調べているかと言うと、私はchildNodesからelementNodeだけの配列を取得するようなメソッドが欲しかったので、作ろうと思っていたのだ。そして、その際に、firstChildからnextSiblingでグルングルン回すか、childNodes分こっちもグルングルン回するかどっちにしようかな、と思っていたのだ。そこで自分でベンチマークを取ってみたよ。まぁ、結局、全てを線形探索するのだから、どっちでもいいような気がするけども。。。で、ベンチマークツールはamachang氏のbenchmark.jsを使用した。

ソースはこんな感じ。ここにサンプルも置いておくよ。childNodes && firstChild + nextSibling Benchmark

< !DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

  
    
    
    
    
    
    
    

  
  

childNodes && firstChild + nextSibling Benchmark

firefox2.0.0.7の私の環境での結果は。。。

preparing ...
let's go!
.
*** initialize ***
result : 2763.966739[ms]
.
*** firstChild + nextSibling childElements ***
result : 680.966739[ms]
.
*** childNodes childElements ***
result : 400.966739[ms]
.
*** firstChild + nextSibling childElement ***
result : 48.991075[ms]
.
*** childNodes childElement ***
result : 51.991075[ms]
.
*** prototype.js down + next ***
result : 440.966739[ms]
.
finish!

childNodesの方が速い。まぁ、あんまり変わらないけど、予想と違うぞ?つーか、downとnextの組み合わせ遅すぎ。
ついでにIE6でも見てみる。

preparing ...
let's go!
.
*** initialize ***
result : 67827.967579[ms]
.
*** firstChild + nextSibling childElements ***
result : 1151.967579[ms]
.
*** childNodes childElements ***
result : 14900.967579[ms]
.
*** firstChild + nextSibling childElement ***
result : 59.089179[ms]
.
*** childNodes childElement ***
result : 3354.967579[ms]
.
*** prototype.js down + next ***
result : 22592.967579[ms]
.
finish!

うーん。洒落になってねぇ。ブラウザが死んだかと思たよ。。。childNodes重いっす。。。まぁ、線形探索っぽくchildNodesのitemを見て回っているので、遅くなることはしょうがないのだけど、なんとかならんかな。。。textNodeをすっ飛ばさなくてもいいのだったら、childNodesでindex指定で一発で取れて速そうだけど、今回の目的のelementNodeだけが欲しいならダメだね。こりゃ。

というわけで、firefoxでは、少しだけchildNodesの方が速かったのだけど、IE6のためにfirstChildとnextSiblingを採用するということになりました。。。つーか、downとnextはさらに使いものにならん。。。

prototype.jsのcleanWhitespaceなんかを見てみると、firstChildからnextSiblingを取っているのは理由があったのね。

DOM操作は自分で書いた方が良さそう。

未だ、prototype.js。しかし、DOM周りはやっぱり重くて使い物にならん。。。nextとかあかんね。引数無しで使っても重すぎ。というわけで、自作でnextElementとかのメソッドを追加するのが良さそうだ。まぁ、とりあえず調べてみた。そしたら、はてなの匿名ダイアリーに結構コッテリしたソースが載っているじゃない?つーか、このレベルのJavaScriptが書けたら、いいよなー。

というわけで、一応リンク。つーか、読みがいもあるし、いいかもしんね。つーか、Tenって何?
http://anond.hatelabo.jp/20070719173038

えーと、特に使えそうだと思ったのは、Ten.DOM周り。このままじゃ使えんけど、まさにやりたかったことが書いてある。

Ten.DOM = new Ten.Class({
    getElementsByTagAndClassName: function(tagName, className, parent) {
        if (typeof(parent) == 'undefined') {
            parent = document;
        }
        var children = parent.getElementsByTagName(tagName);
        if (className) {
            var elements = [];
            for (var i = 0; i < children.length; i++) {
                var child = children[i];
                var cls = child.className;
                if (!cls) {
                    continue;
                }
                var classNames = cls.split(' ');
                for (var j = 0; j < classNames.length; j++) {
                    if (classNames[j] == className) {
                        elements.push(child);
                        break;
                    }
                }
            }
            return elements;
        } else {
            return children;
        }
    },
    removeEmptyTextNodes: function(element) {
        var nodes = element.childNodes;
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];
            if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) {
                node.parentNode.removeChild(node);
            }
        }
    },
    nextElement: function(elem) {
        do {
            elem = elem.nextSibling;
        } while (elem && elem.nodeType != 1);
        return elem;
    },
    prevElement: function(elem) {
        do {
            elem = elem.previousSibling;
        } while (elem && elem.nodeType != 1);
        return elem;
    },
    scrapeText: function(node) {
        var rval = [];
        (function (node) {
            var cn = node.childNodes;
            if (cn) {
                for (var i = 0; i < cn.length; i++) {
                    arguments.callee.call(this, cn[i]);
                }
            }
            var nodeValue = node.nodeValue;
            if (typeof(nodeValue) == 'string') {
                rval.push(nodeValue);
            }
        })(node);
        return rval.join('');
    },
    onLoadFunctions: [],
    loaded: false,
    timer: null,
    addEventListener: function(event,func) {
        if (event != 'load') return;
        Ten.DOM.onLoadFunctions.push(func);
        Ten.DOM.checkLoaded();
    },
    checkLoaded: function() {
        var c = Ten.DOM;
        if (c.loaded) return true;
        if (document && document.getElementsByTagName &&
            document.getElementById && document.body) {
            if (c.timer) {
                clearInterval(c.timer);
                c.timer = null;
            }
            for (var i = 0; i < c.onLoadFunctions.length; i++) {
                    c.onLoadFunctions[i]();
            }
            c.onLoadFunctions = [];
            c.loaded = true;
        } else {
            c.timer = setInterval(c.checkLoaded, 13);
        }
    }
});

でも、私はElementとか思いっきり拡張しちゃうタイプなので、nextElementやpreviousElementは、addMethodしちゃう。ついでに、childElementsなども作っちゃおうかなー。そして、引数に数字を当てて、とっちゃうような感じで作ろうかしらん。他にも、prototype.jsのcleanWhitespace()辺りをrecursiveにする奴とか。

しかし、今は、うちのキーボードが壊れて、「カッコ閉じる」が入らないので、新しいキーボードが来てからにしよう。

まだまだバグが多いのね。

最近は、ずっとJavaScriptを書いていて、世間ではjQueryが注目されている中、私は未だprototype.jsとscript.aculo.usを使っている。今の仕事では結構、面倒くさいこといろいろやっているんだけど、prototype.jsやscript.aculo.usってまだまだバグが多いのね。というか、script.aculo.usの中に入っているprototype.jsが古いっていうのもあるんだろうけど、悩ましいなぁ。ここではまると結局、中のソースを見たりしないといけないので、時間がかかってしょうがない。

で、はまったついでにメモとして書いておく。

  1. Insertion.Afterでtableのtrを複数入れようとしたときに順番が逆になる。
  2. えと、今作っているところの一つはtd要素にrowspanとか使っているので、複数のtr要素を一つの行として扱わないといけないところがあった。
    そこで、rowspan分挿入しようとしたのだが、順番が逆になって入ってしまう。。。前回は、結構バグに悩んだけど、今回からはもう既存のバグをまず探す。そして、解決されていればいつ対応されたのかバージョンを探る。で、今回見つけたのは、これ。

    Insertion.After inserts table rows the wrong way round in IE

    Prototype.jsでは、1.5.2でFixedらしい。ということは、script.aculo.usと同時に使えないね。アプリ側でPrototype.Versionを見て、1.5.2より下の場合のみ拡張するコードを書いたよ。

  3. draggableな要素がoverflowのhidden, scroll, auto(たぶん)のときには、そのoverflowを指定してある要素の外では見えなくなっちゃう。
  4. えと、これはかなりはまった。。。。orz….divでoverflow:scrollな要素が二つあって、そこの間をドラッグドロップをしようとしていたのだけど、移動先のdivに入るとドラッグレイヤーの下に入ったかのように見えて、消えちゃうの。で、いろいろ探した結果、結構ホットなバグみたい。IE6ではちゃんと動くけど、mozillaとIE7ではダメというちょっと不思議なバグ。つーか、IE6の動作がそもそもいかんのかな。。。
    draggables are stuck in a div with overflow:scroll. A way to move them to upmost dom level needed

    Dragging an element outside a overflow:auto container

    Drag draggables outside an overflow:auto div to an external droppable container

    で、解決方法は結構大変。えと、問題なのは、
    draggables are stuck in a div with overflow:scroll. A way to move them to upmost dom level needed

    I discussed this with Thomas some time ago, and the problem is that the element we move (be it in ghosting mode or not) usually must remain at the same DOM level because otherwise, styles might get lost. I’m working on a solution right now, too, it will involve a new option you can use to assign a class name to the element while it’s being dragged. If this class name is supplied, the element will be attached to the body. Let’s see if this works, I’ll provide the patch as soon as it is ready

    ということなのだ。つまり、z-indexを付けようがどうだろうが、ダメなものはダメなのだ。解決方法としては、body要素の下にdraggableな要素をappendChildしてやればいいのだが、それをするとstyleが無くなる可能性ががあるというわけ。もちろん位置も合わせないといけないしね。つまり、ドラッグする要素のstyleがそれに付けられているclassやidなどで完結していればいいのだが、そうでないときは、いきなりbody要素の下に付けてもさぁ、困ったな、といったことが起きるのだ。li要素とか。。。で、未だ正しい解決方法は出てきていない。まぁ、こういうところがハックのしがいがあるんだよね。

    今回の私が使用したかったところは、div要素で完結していたので、width指定とかもしておけば、body要素の下に置いても大丈夫だった。後は、Positionの位置を合わせるだけ。
    Draggables.addObserverのonStartとonEndを使用して、要素をbody直下に変えたり、戻したりしたらいけた。位置がずれるところはちょっと泥臭いことしたけど、もう少しキレイに書けたらここに載せるかもしれん。

しかし、
Super.mario.bros
辺とか見ると自分はまだまだっす。。。先は長いなー。

Shin Ohno 2003-2012