コラムです。
まだまだひよっこだった頃、JSの遅延処理とCSSアニメーションについてあれこれ考えていた時の話です。
JSの遅延処理についてあれこれ考えていたこと
次のような処理をしたかった時の話。
2. 1秒後にconsole.log(2)
3. さらに1秒後にconsole.log(3)
まず、明らかに間違ってるパターンのやつ。
パターン1 明らかな間違い
console.log(1); setTimeout(function(){ console.log(2); }, 1000); setTimeout(function(){ console.log(3); }, 1000);
→サンプル
結果は、console.log(1)の後、console.log(2)とconsole.log(3)が同時。
これは明らか。
であればと、
パターン2 入れ子
console.log(1); setTimeout(function(){ console.log(2); setTimeout(function(){ console.log(3); }, 1000); }, 1000);
→サンプル
もしくは、
パターン3 時間差
console.log(1); setTimeout(function(){ console.log(2); }, 1000); setTimeout(function(){ console.log(3); }, 2000);
→サンプル
これで思うような動きが実装できた!
でもこれらはいずれも悪い例。
まずパターン2は、遅延処理が多くなればなるほど入れ子構造が深くなっていき、メンテナンス性が悪くってしまう。しかもsetTimeoutで同時に複数のタイマーを回すということは、CPUに負荷をかけることになる。
パターン3は、同時に複数のタイマーを回しているという点で同様によろしくない。しかも、同時進行するタイマーが寸分の狂いなく動く保証もない。つまりそれは確実なコールバックにはなり得ない。メンテナンス性が悪くなるのはいわずもがな。
うーん、どうしようか?
JavaScriptはDOMに働きかけるものである。最適に細分化されたメソッドを定義して、それらをイベントにバインドするのが正しい。
つまり、メソッドチェーンで実行させるのが良い。
ということで、次のパターン。
パターン4 遅延メソッドをオブジェクト化
function Delay(method) { if (!(this instanceof arguments.callee)) { return new arguments.callee(method); } this.time = 0; for (key in method) { var f = method[key]; this[key] = (function (f) { return function () { var args = arguments; setTimeout(function () { f.apply(method, args); }, this.time); return this; } })(f); } } Delay.prototype.await = function (t) { this.time += t; return this; }; var init = function(){}; init.prototype = { consoleLog : function(i) { console.log(i); }, }; var hoge = new init(); Delay(hoge).consoleLog(1).await(1000).consoleLog(2).await(1000).consoleLog(3);
→サンプル
これはなにをやっているかというと、遅延処理させたい関数オブジェクトを、Delayクラスのコンストラクタに渡して、Delay自身のメソッドにしてしまう。そしてメソッドチェーンで実行!的な。
「メソッド定義→イベントにバインド→メソッドチェーンで実行」という一連の流れを守っていて、すっきり見えるが、それはあくまでも見た目だけ。
実行される処理自体はまるで変わらないし、むしろ関数オブジェクトをメソッドにセットしている分ダメな気もする。
パターン5 jQuery.Deferred
jQuery.Deferredとは、jQueryのバージョン1.5から導入された、非同期処理をうまく扱うための標準モジュール。
jQuery依存症からの脱却を図っていた時期だけに、またjQueryかよ…って気はしたけど、これがけっこう使える。
jQuery.Deferredの詳しい説明はこちらの記事など参考に: 爆速でわかるjQuery.Deferred超入門
$(function(){ function consoleLog(i, time) { var d = new $.Deferred; setTimeout(function() { console.log(i); d.resolve(); }, time); return d.promise(); } $("#start").on("click", function(){ $.when(function() { console.log(1); }()) .then(function() { return consoleLog(2, 1000); }) .then(function() { consoleLog(3, 1000); }) }) });
→サンプル
メソッドチェーンで連結した処理のコールバックを検知しながら実行できる。
もちろん遅延処理以外の通常処理を連結することも可能。
ということで、現段階ではこれを正解にした。
とは言え、Deferredオブジェクトをnewしまくるのはよくないので、さじ加減は必要。
CSSアニメーションのコールバックについてあれこれ考えていたこと
じゃあ次はCSSアニメーションのコールバックを検知したいなぁ、と。
まず、CSS3の時代的には、transitionプロパティで出来ることはCSSでやる!っていうのを前提にするべきだと思っている。いままでJavaScript、例えばjQueryのanimateメソッドでやっていたことは、だいたいCSSでもできる。
じゃあどちらでもいいじゃん、ではなく、やはりJavaScriptってのは所詮ハックだということを忘れてはいけない。
CSSに比べてJavaScriptは遅い。jQuery前提だともっと遅い。
一方CSSはブラウザが直接解釈できるのでめっちゃ速い。だからなるべくtransitionを使うべきだと思う。
さて、次のようなことをしたかった時の話。
2. 1秒かけてblueからredにする
3. 完了したらconsole.log(“end”);
そもそもCSSアニメーションのコールバックをJavaScriptで検知できるのか?出来ないだろ…ということをあれこれ。
いろいろ調べてみた結果、Argumentsクラスのcalleeプロパティを使うと出来るっぽい。
<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title></title> <style type="text/css"> #hoge { width: 100px; height: 100px; background-color: blue; } #hoge.active { -webkit-animation-name: step; -webkit-animation-duration: 1s; -webkit-animation-timing-function: ease; background-color: red; } @-webkit-keyframes step { 0% { background-color: blue; } 100% {background-color: red; } } </style> </head> <body> <div id="hoge"></div> <script type="text/javascript"> var element = document.getElementById("hoge"); element.addEventListener("webkitAnimationEnd", function(e){ e.currentTarget.removeEventListener("webkitAnimationEnd", arguments.callee, false); console.log("end"); }, false); element.className = "active"; </script> </body> </html>
→サンプル
できた!
ただちょっと懸念は残る。より複雑で相互的なアニメーションの場合はけっこうめんどうそうだなぁ、と。
でも、できた!!
なんてことをキャッキャッ言ってたら、ある人に「それってBootstrapのtransition.jsじゃん」と言われたんですね。
その時は正直「Bootstrapのtransition.js?なにそれ?BootstrapってただのCSSフレームワークじゃないの?」って感じだった。
その後、Bootstrapのソースコードを読んでみたら、、すごいなこれ、ってなって。
BootstrapってCSSフレームワークとJavaScriptコンポーネントのライブラリ群で、簡単に流行のUIが実装できるだけのものだと思っていた。確かにその通りなんだけど、このすべての機能を実現する根底にあるのがtransition.jsだった。
Bootstrapのドキュメントは簡略化が進んでいて、ver.2.0からはLESSのドキュメントも省かれるくらいになってきている。ましてtransition.jsについてのドキュメントなんて最初からなかったと記憶している。これが一番すごいのに…
「ドキュメントに書いてないから知らなかった…」というのは、いわゆる巷で増殖中の自称Webデザイナーの言うこと。もしくは、jQueryライブラリを拾ってくるだけでJavaScript書けます的なことを言っちゃうレベルの人の言うことだったな、と深く反省したわけです。
ということで、結果的にはBootstrapのtransition.jsを知っていれば、なにもここまであれこれ考えることはなかったのですが、それまでの試行錯誤がなければ、本質的な理解の深さはまったく違っていただろうと思います。
昨今はオープンイノベーションの名のもとに、便利で高機能なフレームワークやライブラリが世の中にたくさん公開されています。非常に歓迎すべきことですが、その弊害もあります。そういうフレームワークやライブラリをそのまま使えばなんとなく実装はできるけれども、いざという時に中身のソースコードをまったく読めないとか、ゼロからではまったく書けないとか、そういう人が増えている現状は嘆かわしい。
そういう人たちにはそういう人たちなりのプライオリティが存在していて、なにもプログラムが書けるだけが正義ではないのだろうけど、少なくとも、本当にJavaScriptが出来る人っていうのは、自ら試行錯誤を繰り返したり、より深い探究心を持っているんだろうなぁ、と身にしみて実感したというお話でした。