ぶれすとつーる

だいたいjavascript

アニメーションとUIスレッド

前回の記事(setTimeoutとUIスレッドを学ぶよ JS Advent Calendar, オレ標準コース)で紹介したUIスレッドの概念でアニメーションをみてみる。

前回の復習

UIスレッド
UIスレッドとはjsとUIの更新が行われるプロセスのことである。

UIスレッドはただのキューイングシステムでプロセスがアイドル状態になるまでタスクを保持する。

プロセスがアイドル状態になったら次のタスク(UI更新やjsの実行)をキューから取り出して逐次的に実行していくものである。

これをもとにアニメーションがいかに描画されているのかをみてみる。


今回はこんな感じのアニメーションについて。

【demo】 http://jsfiddle.net/nazomikan/7RJWU/

(function (win, doc) {
    var run = doc.getElementById('run'),               // 発火ボタン
        box = doc.getElementById('animate-box'),  // 動く要素
        frame = doc.getElementById('frame');         // 親要素
    
    run.addEventListener('click', function(){
        var start = +new Date(),
            wide = frame.offsetWidth - box.offsetWidth,
            id;
        
        id = setInterval(function () {
            var elapsed = +new Date() - start;
            if (elapsed > 1000) {
                clearInterval(id);
                elapsed = 1000;
            }
            box.style.left = wide * (elapsed / 1000) + 'px';
        },10);
        
    },false);

}(window, document));

これは「うごくよ」ボタンを押すと1秒かけて親要素の左端から右端までアニメーションしていくコードです。

簡単に説明すると発火要素がクリックされた時に親要素の横幅の長さを取得して、10msごとにanimate-box要素の左の位置座標を増やして親要素の横幅いっぱいまで移動させる処理です。


単純に座標を増やしていくだけに見えるのでこれをwhileループに置き換えてみましょう。

(function (win, doc) {
    var run = doc.getElementById('run'),
        box = doc.getElementById('animate-box'),
        frame = doc.getElementById('frame');
    
    run.addEventListener('click', function(){
        var start = +new Date(),
            wide = frame.offsetWidth - box.offsetWidth,
            elapsed;
        
        while ((elapsed = +new Date() - start) < 1000) {
            box.style.left = wide * (elapsed / 1000) + 'px';
        }

    },false);

}(window, document));

ずいぶんとすっきりしました。

さてデモをみてみましょう

【demo】 http://jsfiddle.net/nazomikan/fcL5H/


さわってみれば分かると思いますが、思ったとおりには動いていません。

クリックするとブラウザが固まって、1秒がたったとおもったら要素は移動しおわっています。

これは何故かというとUIスレッドをブロッキングしてしまっているからです。

図でみるとこんな感じです。

jsの実行も画面の更新も全てUIスレッドで行われるため、while文のように連続して実行される処理を行う場合UIスレッドはその処理が終わるまでの間、完全にブロックされてしまい、他の処理(ここではブラウザの更新)の入る余地がないわけです。

そのため要素の移動が終わったタイミング(1秒経過後)にいっきに描画されているわけです。


一番最初のコードを見直してみましょう。

【demo】 http://jsfiddle.net/nazomikan/7RJWU/

(function (win, doc) {
    var run = doc.getElementById('run'),               // 発火ボタン
        box = doc.getElementById('animate-box'),  // 動く要素
        frame = doc.getElementById('frame');         // 親要素
    
    run.addEventListener('click', function(){
        var start = +new Date(),
            wide = frame.offsetWidth - box.offsetWidth,
            id;
        
        id = setInterval(function () {
            var elapsed = +new Date() - start;
            if (elapsed > 1000) {
                clearInterval(id);
                elapsed = 1000;
            }
            box.style.left = wide * (elapsed / 1000) + 'px';
        },10);
        
    },false);

}(window, document));

これはsetIntervalを利用してアニメーションを作っています。

setIntervalは第2引数に設定された値msごとに第1引数に指定された関数をUIスレッドに追加するというものです。

つまりUIスレッドはこんな感じになっています。


このようにアニメーションを行うときは座標を更新するごとにアイドル状態を作らないと全く画面上でアニメーションしてるように見えないのできをつけて><