Hatena::ブログ(Diary)

latest log このページをアンテナに追加 RSSフィード

2011-09-25

(ε・◇・)з CoffeeScript と mofmof.js

(ε・◇・)з CoffeeScriptRubyPython っぽく書けて JavaScriptコンパイルできるプリティな言語だとか!

(ε・◇・)з 噂では CoffeeScript で書くとコードが短くなるとか!!

物は試しに http://jashkenas.github.com/coffee-script/ にある幾つかのコード片と、mofmof.js 混じりで書いた場合のコードを比べてみました。

(ε・◇・)з mofmof.js はここだよ → http://code.google.com/p/mofmof-js/w/list

Syntax
// coffee

    # Assignment:
    number   = 42
    opposite = true

    # Conditions:
    number = -42 if opposite

    # Functions:
    square = (x) -> x * x

    # Arrays:
    list = [1, 2, 3, 4, 5]

    # Objects:
    math =
      root:   Math.sqrt
      square: square
      cube:   (x) -> x * square x

    # Splats:
    race = (winner, runners...) ->
      print winner, runners

    # Existence:
    alert "I knew it!" if elvis?

    # Array comprehensions:
    cubes = (math.cube num for num in list)

// coffee -> js ----------------------------

    var cubes, list, math, num, number, opposite, race, square;
    var __slice = Array.prototype.slice;
    number = 42;
    opposite = true;
    if (opposite) number = -42;
    square = function(x) {
      return x * x;
    };
    list = [1, 2, 3, 4, 5];
    math = {
      root: Math.sqrt,
      square: square,
      cube: function(x) {
        return x * square(x);
      }
    };
    race = function() {
      var runners, winner;
      winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
      return print(winner, runners);
    };
    if (typeof elvis !== "undefined" && elvis !== null) alert("I knew it!");
    cubes = (function() {
      var _i, _len, _results;
      _results = [];
      for (_i = 0, _len = list.length; _i < _len; _i++) {
        num = list[_i];
        _results.push(math.cube(num));
      }
      return _results;
    })();

// mofmof ----------------------------

    // Assignment:
    var number = 42, opposite = true;

    // Conditions:
    opposite && (number = -42);

    // Functions:
    function square(x) { return x * x; }

    // Arrays:
    list = [1, 2, 3, 4, 5];

    // Objects:
    math = {
        root:   Math.sqrt,
        square: square,
        cube:   function(x) { return x * square(x); }
    };

    // Splats:
    function cube(winner, runners, /*... */) {
        return print.apply(null, arguments);
    }

    // Existence:
    elvis != null && alert("I knew it!");

    // Array comprehensions:
    cubes = list.filter(math.cube);

(ε・◇・)з 基本 Syntax だと、あまり変わった感じしませんね〜

Functions
// coffee

    square = (x) -> x * x
    cube   = (x) -> square(x) * x

    fill = (container, liquid = "coffee") ->
      "Filling the #{container} with #{liquid}..."

// coffee -> js ----------------------------

    var cube, square;
    square = function(x) {
      return x * x;
    };
    cube = function(x) {
      return square(x) * x;
    };
    var fill;
    fill = function(container, liquid) {
      if (liquid == null) liquid = "coffee";
      return "Filling the " + container + " with " + liquid + "...";
    };

// mofmof.js ----------------------------

    function square(x) { return x * x; };
    function cube(x)   { return square(x) * x; }

    function fill(container, liquid /* = coffee */) {
        return "Filling the @@ with @@...".f(container, liquid || "coffee");
    }

(ε・◇・)з Coffee は function が -> になってる分だけタイプ数少なめですね

Loops and Comprehensions
// coffee
    # Eat lunch.
    eat food for food in ['toast', 'cheese', 'wine']

// coffee -> js ----------------------------

    var food, _i, _len, _ref;
    _ref = ['toast', 'cheese', 'wine'];
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      food = _ref[_i];
      eat(food);
    }

// mofmof ----------------------------

    ['toast', 'cheese', 'wine'].forEach(eat);

            or

    var iter = Hash(['toast', 'cheese', 'wine']), food;
    while (food = iter.next()) {
        eat(food);
    }

(ε・◇・)з mofmof.js はforEachやイテレータ使ってます。実行速度が重要でなければ、このへんは好みかな〜

// coffee

    countdown = (num for num in [10..1])

// coffee -> js ----------------------------

    var countdown, num;
    countdown = (function() {
      var _results;
      _results = [];
      for (num = 10; num >= 1; num--) {
        _results.push(num);
      }
      return _results;
    })();

// mofmof ----------------------------

    countdown = 10..$(1);

(ε・◇・)з mofmof.js は Number.prototype.$ を拡張して 10..$(1) で 10 から1までの配列を生成していますよ〜

// coffee

    yearsOld = max: 10, ida: 9, tim: 11

    ages = for child, age of yearsOld
      "#{child} is #{age}"

// coffee -> js ----------------------------

    var age, ages, child, yearsOld;
    yearsOld = {
      max: 10,
      ida: 9,
      tim: 11
    };
    ages = (function() {
      var _results;
      _results = [];
      for (child in yearsOld) {
        age = yearsOld[child];
        _results.push("" + child + " is " + age);
      }
      return _results;
    })();

// mofmof ----------------------------
    var yearsOld = { max: 10, ida: 9, tim: 11 };
    Hash.map(yearsOld, function(age, child) {
        return "@@ is @@".f(child, age);
    });

(ε・◇・)з Coffee側のコードはシンプルですね〜


Array Slicing and Splicing with Ranges
// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    copy    = numbers[0...numbers.length]

    middle  = copy[3..6]

// coffee -> js ----------------------------
    var copy, middle, numbers;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    copy = numbers.slice(0, numbers.length);
    middle = copy.slice(3, 7);

// mofmof ----------------------------
    var numbers = 0..$(9);

    var copy    = numbers.clip(0, numbers.length);

    var middle  = copy.clip(3, 6);

(ε・◇・)з もふもふ!

// coffee
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    numbers[3..6] = [-3, -4, -5, -6]

// coffee -> js ----------------------------
    var numbers, _ref;
    numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    [].splice.apply(numbers, [3, 4].concat(_ref = [-3, -4, -5, -6])), _ref;

// mofmof ----------------------------
    numbers = 0..$(9);
    numbers.swap(3, (-3).$(-6));

(ε・◇・)з mofmof 側は、Array#swap配列の一部をごっそり入れ替えてます〜


Everything is an Expression (at least, as much as possible)
// coffee
    globals = (name for name of window)[0...10]

// coffee -> js ----------------------------
    var globals, name;
    globals = ((function() {
      var _results;
      _results = [];
      for (name in window) {
        _results.push(name);
      }
      return _results;
    })()).slice(0, 10); // 全部とってきて10個だけ返す

// mofmof ----------------------------
    globals = Hash.keys(window, 10); // 10個とってきて返す

(ε・◇・)з mofmof の Hash.keys は、第二引数で列挙する要素の最大数を指定できるので、10個だけ列挙して返します〜


Classes, Inheritance, and Super
// coffee

    class Animal
      constructor: (@name) ->

      move: (meters) ->
        alert @name + " moved #{meters}m."

    class Snake extends Animal
      move: ->
        alert "Slithering..."
        super 5

    class Horse extends Animal
      move: ->
        alert "Galloping..."
        super 45

    sam = new Snake "Sammy the Python"
    tom = new Horse "Tommy the Palomino"

    sam.move()
    tom.move()


// coffee -> js ----------------------------
    var Animal, Horse, Snake, sam, tom;
    var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
      for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
      function ctor() { this.constructor = child; }
      ctor.prototype = parent.prototype;
      child.prototype = new ctor;
      child.__super__ = parent.prototype;
      return child;
    };
    Animal = (function() {
      function Animal(name) {
        this.name = name;
      }
      Animal.prototype.move = function(meters) {
        return alert(this.name + (" moved " + meters + "m."));
      };
      return Animal;
    })();
    Snake = (function() {
      __extends(Snake, Animal);
      function Snake() {
        Snake.__super__.constructor.apply(this, arguments);
      }
      Snake.prototype.move = function() {
        alert("Slithering...");
        return Snake.__super__.move.call(this, 5);
      };
      return Snake;
    })();
    Horse = (function() {
      __extends(Horse, Animal);
      function Horse() {
        Horse.__super__.constructor.apply(this, arguments);
      }
      Horse.prototype.move = function() {
        alert("Galloping...");
        return Horse.__super__.move.call(this, 45);
      };
      return Horse;
    })();
    sam = new Snake("Sammy the Python");
    tom = new Horse("Tommy the Palomino");
    sam.move();
    tom.move();

// mofmof ----------------------------
    mm.Class("Animal", {
        init: function(name) {
            this.name = name;
        },
        move: function(meters) {
            alert("@@ moved @@m.".f(this.name, meters));
        }
    });
    mm.Class("Snake:Animal", {
        move: function() {
            alert("Slithering...");
            this.superCall("move", 5);
        }
    });
    mm.Class("Horse:Animal", {
        move: function() {
            alert("Galloping...");
            this.superCall("move", 45);
        }
    });
    var sam = mm("Snake", "Sammy the Python");
    var tom = mm("Horse", "Tommy the Palomino");

    sam.move();
    tom.move();

(ε・◇・)з mofmof にもクラスあるんだよ〜

(ε・◇・)з 最大継承数は1段に限定してある(昔は3段までサポートしてた)けど、1段継承できればニーズの9割カバーできるからOKなんだよ〜 (それ以上継承したい人は変態的なコードを書きたい人だけだと思うよ〜

Destructuring Assignment
// coffee
    theBait   = 1000
    theSwitch = 0

    [theBait, theSwitch] = [theSwitch, theBait]

// coffee -> js ----------------------------
    var theBait, theSwitch, _ref;
    theBait = 1000;
    theSwitch = 0;
    _ref = [theSwitch, theBait], theBait = _ref[0], theSwitch = _ref[1];

// mofmof ----------------------------
    var theBait = 1000, theSwitch = 0;

    [theBait, theSwitch].swap(0, [theSwitch, theBait]);

(ε・◇・)з ここでも Array#swap が出てきてます〜


Chained Comparisons
// coffee
    cholesterol = 127

    healthy = 200 > cholesterol > 60

// coffee -> js ----------------------------
    var cholesterol, healthy;
    cholesterol = 127;
    healthy = (200 > cholesterol && cholesterol > 60);

// mofmof ----------------------------
    var cholesterol = 127

    healthy = cholesterol.inRange(200, 60);

(ε・◇・)з Coffee のほうが分かりやすいね〜

おしまい

(ε・◇・)з mofmof.js も結構イケてるんじゃないでしょうか!

2010-08-05

Flashのように滑らかなアニメーションを実装するには(uupaa.js vs jQueryデモ)

f:id:uupaa:20100805051716p:image

JavaScriptFlash のような滑らかなアニメーションを行うためには、クロスブラウザな知識の他に、GC(ガベージコレクション)や「どうすれば安定した品質がだせるのか」といったスキルが求められます。

# GC の話は WEB+DB PRESS 57 でちょっと書いてます。

派手目なアニメーションが目的で jQuery を採用している方もいるとは思いますが、実は uupaa.js でも アニメーションや easing が利用できるんです。あらびっくり。

ただ「機能がありますよ〜」だと、さみしいので

みんな大好き jQueryuupaa.js のアニメーションの品質の違いが分かるようなデモを作成してみました。

iPhone/iPad や、IE6IE8で opacity を切り替えて見ると、ハッキリと違いが分かると思います。

http://jsdo.it/uupaa/uufxVsjQueryAnimate/fullscreen

jQuery に無い機能として、uupaa.js には 最適化されたカラーアニメーションや、ある期間からある期間までを指定してリバースするリバースアニメーションの機能などもあります。

どんなことやってるか

長くなるのでポイントだけ。

http://code.google.com/p/uupaa-js/source/browse/trunk/0.8/src/uupaa.js?r=809#1760

// uu.interval - interval timer
function uuinterval(callback, // @param Function: callback
                    arg) {    // @param Mix: arg
                              // @return Number: id
    var id = uuguid();

    uuinterval.ary.push(id, callback, arg);
    if (uuinterval.base === null) {
        uuinterval.base = setInterval(tick, _ver.chrome ? 4 : 12);
    }
    return id;
}
uuinterval.ary = [];    // [<id, callback, arg>, ...]
uuinterval.base = null; // base interval timer id

// inner - tick interval timer
function tick() {
    var ary = uuinterval.ary, i = 0, iz = ary.length;

    for (; i < iz; i += 3) {
        // callback(timer.id, arg) -> false is loopout
        if (ary[i + 1](ary[i], ary[i + 2]) === _false) {
            ary.splice(i, 3);
            i  -= 3;
            iz -= 3;
        }
    }
}

# uuinterval.ary と uuinterval.base は LookDown です。WEB+DB PRESS 57 でちょっと書いてます。

  • アニメーションキューは一つの setInterval で回している。Chromeなら4ms, 他のブラウザでは12ms間隔
  • 内部関数 tick が定期的に呼ばれる
  • データ構造が { id: [callback, arg], ... } な Hash ではなく、フラットな配列(uuinterval.ary)で組んでいるのは、for in ループが、for ループよりも高コストなため(特にIEで)
  • callback が false を返すと、Array.splice() で該当する情報だけ削ってる。
    • Array.splice() はインデックスの張替が発生するため高コスト。
    • 本来であれば、フラグを立ててループの外で uuinterval.ary を再構築すべきだが、大量の要素を扱うケースもありうるので、ループをもう一度まわすコスト + 配列を再生成するコスト ≧ Array.splice() と判断。
      • MobileWebKitモードではコード量の制限もあるため、ダラダラとコードが増やせず最小手でロジックを書く必要があることからも、splice()でやってる

タイマー周りのバックグラウンドはこんな感じです。

追記: Chromeなら4ms, 他のブラウザでは12ms間隔 → これは記事を書いた当時の値ですね。

HTML5を取り込んだ Firefox5, IE10pp2 では setTimeout が最小 4ms, setInterval が最小 10ms (だったかな?) で動作します。また、そもそも window.requestAnimationFrame の利用が推奨されており、setInterval は requestAnimationFrame が利用できない場合に仕方なく使う形になります。requestAnimationFrame は、既に IE10pp2, Firefox4+, Chrome で利用可能です。

タイマーから呼び出される関数も、かなり頑張ってる

アニメーションの品質を確保するためにはタイマー周りだけ頑張ってもだめで、タイマーから定期的に呼ばれる関数でも、かなり頑張ってます。

タイマーから定期的に呼ばれる関数は、uufxbuild でアニメーション開始時に動的に生成しています。

ポイントは、

  • コストの最小化
    • new Function() で文字列から関数を構築
    • 関数呼び出しを限界まで排除。easing 関数もインライン化
    • 与えられた引数だけを参照し、外部スコープを参照しない

などです。詳しくはソースをご覧ください(軽い黒魔法ですが)。

iPhoneなどリソースが限定されている環境だと、jsdo.it 上のサンプルは、コード本来の性能が出せないようなので、実際の動きは http://code.google.com/p/uupaa-js/wiki/uu_fx#Test_Code をご覧ください。

2010-07-30

コードを削る

Twitter だとちょっと厳しいので、こっちでまとめ。

MobileWebKit に対応しようと思ったら、コードをなんとかして削る必要があります。

AND 演算子 で 4byte 削る(10byte → 6byte)

if (a) {
  b();
}
   ↓

a && b();

OR 演算子 で 5byte 削る(11byte → 6byte)

if (!a) {
  b();
}
   ↓

a || b();

ドット演算子以降を自力で Minify する

JavaScriptは動的な言語なので、ドット演算子以降は基本的に minify しても圧縮されません。

document.createElement("div");
document.createElement("a");
document.createElement("p");

   ↓ このままだと、minify しても縮まらない

document.createElement("div");document.createElement("a");document.createElement("p")

ドット演算子によるアクセス( Object.property )は、Object["property"] のシンタックスシュガー(糖衣構文)です。

これらは、同じ処理速度でほぼ同じ価値を持ち、見た目がちょっと違うだけ(ドット演算子は制限も多いけど)。

そこで minifier が処理しやすいように、シンタックスシュガーを解除すると

var doc = document, createElement = "createElement";

doc[createElement]("div");
doc[createElement]("a");
doc[createElement]("p");

   ↓ minify

var a=document,b="createElement";a[b]("div");a[b]("a");a[b]("p")

この辺が最大の削りポイントですが、調子こいてやり過ぎるとコードがみるみる汚くなります。

ライブラリ書くのって大変ですね。

2010-07-17

ワンライナーIE6殺処分(精神的ブラクラ)

f:id:uupaa:20100717112101p:image

<script>document.createStyleSheet().addRule("*>*", "color:red");</script>

このようなコードが埋め込まれたページをIE6で開くと、10数秒でメモリとswapを食い尽くします。PCが「ぎゃふん」って言うので、気をつけてください。

何も作業していない(再起動OKな)状態で実行することをおすすめします。まちがっても、上司に提出しなきゃならない Word や、社内で共有している Excel シートを開いたまま、ネタを仕込んだページを開かないようにしてください。

ぎゃふんっぷりを十二分に堪能した後は、タスクマネージャからIE6を殺してください。メモリを食いつぶした後はCPU利用率が50〜80%ぐらいに下がるため、タスクマネージャも起動できるはずです。

IE6を殺すと下の図のようにメモリが開放されますが、開放されていないリソースもあるらしく、Windows全体がモッサリしてしまいます。そのような場合はPCを再起動してください。

IE6がサポートしていないCSSセレクタであれば上記現象は発生するようなので "*>*" を "div>p" と置き換えても発生します。

えーと

IE8やIE9pp3の互換モードでは発生しないため、実は対策済みなんだけど、IE6では他のバグと同じく仕様で絶賛放置ってことなんでしょうか?

2010-07-08

JIT搭載ブラウザでは変数の再代入コストは無視できる

JIT搭載ブラウザでは a = a; など同じ変数に対する代入を繰り返しても遅くならず、JIT非搭載ブラウザだと a = a; で遅くなります。

JIT非搭載

Browsera=anop
Firefox31177666
IE820361265
IE642913406

JIT搭載

Browsera=anop
Firefox3.6236234
Safari5696702
Opera10.609701028
Google Chrome6445451

    // a = a
    function _a_a(idx) {
        var a = idx;

        a = a;
        a = a;
        a = a;
        a = a;
        a = a;
        a = a;
        a = a;
        a = a;
        a = a;
        a = a;

        return idx + a;
    }

    // nop
    function _nop(idx) {
        var a = idx;

        a = a;

        return idx + a;
    }

なーんの役にも立たないムダ知識をあなたに