AngularJSのdeferredで並列実行

jQueryの$.whenみたいに複数のdeferredを同時に処理するみたいのはAngularJSだと$q.allでできる。

var d1 = $q.defer();
var d2 = $q.defer();
var d3 = $q.defer();
$timeout(function() {
  console.log('d1');
  d1.resolve('d1');
}, 10);
$timeout(function() {
  console.log('d2');
  d2.resolve('d2');
}, 1000);
$timeout(function() {
  console.log('d3');
  d3.resolve('d3');
}, 200);

$q.all([ d1.promise, d2.promise, d3.promise ]).then(function(result) {
  console.log(result); //=> ['d1', 'd2', 'd3']
});

引数は配列でもオブジェクトでもいける。

$q.whenというのもあるけどこれはオブジェクトを$qのdeferredにラップするみたいな感じっぽいので並列処理ができるやつではない。

AngularJSのControllerをネストしたときのスコープ

<div ng-controller="ParentCtrl">
  <div ng-show="isShow">foo</div>

  <div ng-controller="ChildCtrl">
    <button ng-click="toggle()">click</button>
  </div>
</div>

こういうHTMLがあったときに、ChildCtrlから$scope.isShowを操作しても反映されない。

これだとダメ。

var app = angular.module('app', []);

app.controller('ParentCtrl', function($scope) {
  $scope.isShow = false;
});

app.controller('ChildCtrl', function($scope) {
  $scope.toggle = function() {
    $scope.isShow = !$scope.isShow;
  };
});

$scope.$parent.isShowの値を更新ればいける。

app.controller('ParentCtrl', function($scope) {
  $scope.isShow = false;
});

app.controller('ChildCtrl', function($scope) {
  $scope.toggle = function() {
    $scope.$parent.isShow = !$scope.isShow;
  };
});

どうやら

$scope.__proto__ === $scope.$parent

となっているようで、文字通り親のコントローラーを継承しているようになってるみたい。

なのでChildCtrlで$scope.isShowの値を参照しようとすると親のコントローラーの値を参照できるんだけど、代入すると自分自身のプロパティとして新しくつくるんで親には影響しない、ということかな。

なので、以下のように更新する値をオブジェクトにするといける。

var app = angular.module('app', []);

app.controller('ParentCtrl', function($scope) {
  $scope.data = { isShow: false };
});

app.controller('ChildCtrl', function($scope) {
  $scope.toggle = function() {
    $scope.data.isShow = !$scope.data.isShow;
  };
});

これは$scope.dataを新しくつくるのでなく$scope.__proto__.dataを更新するからだね。

rubyの正規表現の後方参照

Ruby 1.9以降では正規表現のキャプチャに名前がつけられるのでこういう書き方ができる。

img = 'foo.png'
img_on = img.sub /\.(?<ext>\w+)$/, '_on.\k<ext>'
p img_on #=> foo_on.png

(?...) でキャプチャして \k で参照できる。$1 とか \1 みたいな意味不明な記号使わないで済むのでわかりやすくなる。

hub pull-requestでブランチを指定する

これ見て試してみたら、devブランチに対してpull requestしたいのにmasterブランチに対してしてしまって悲惨なことになった。(そしてこの操作は戻せないという・・)
http://qiita.com/kyanny/items/170a188a87925f81ae86

デフォルトはmasterだから -b オプションで指定してねってことらしい。
https://github.com/github/hub/issues/154

$ hub pull-request -i 100 -b dev

でいけた。

Mongooseのバージョニング

Mongooseは3からバージョニングという機能が入ったんだけどよくわかってなかったので調べたメモ。

詳しくはここに書いてある。
http://aaronheckmann.tumblr.com/post/48943525537/mongoose-v3-part-1-versioning

バージョニングを使うと何がいいかというと、配列でネストしたスキーマをもつ場合などに、更新や削除の処理が並列に走ると要素がずれるという問題が解決できるらしい。

次のような検証スクリプトを書いた。

var mongoose = require('mongoose')
  , Schema = mongoose.Schema
  , ObjectId = mongoose.Schema.ObjectId;

mongoose.connect('mongodb://localhost/version_key_test', function(err) {
  console.log(err);
});
mongoose.set('debug', true);
  
var commentSchema = new Schema({ 
    body: String
  , user: String
  , created: { type: Date, default: Date.now }
});
var postSchema = new Schema({ comments: [commentSchema] });
var Post = mongoose.model('Post', postSchema);

Post.remove(function() {
  var post = new Post();
  post.comments.push({ body: '1', user: '1'});
  post.comments.push({ body: '2', user: '2' });
  post.comments.push({ body: '3', user: '3' });

  var commentId = post.comments[1]._id;

  post.save(function(err, post) {
    // 最初の要素を削除
    Post.findById(post._id, function(err, post) {
      post.comments.shift();
      post.save(function(err) {
        if (err) console.log(err);
      });
    });

    // 二番目の要素を更新
    Post.findById(post._id, function(err, post) {
      setTimeout(function() {
        var comment = post.comments.id(commentId);
        comment.body = 'new 2';
        post.save(function(err) {
          if (err) console.log(err);
        });
      }, 100);
    });
  });
});

これはコメントの削除と更新が同時に行われたケースを再現してる。これを実行すると、Mongoose v2.xでは意図せぬ挙動になる。具体的には次のような結果になる。

> db.posts.find().pretty()
{
  "_id" : ObjectId("5215ddc11c18b90000000002"),
  "comments" : [
    {
      "body" : "2",
      "user" : "2",
      "_id" : ObjectId("5215ddc11c18b90000000004"),
      "created" : ISODate("2013-08-22T09:45:37.903Z")
    },
    {
      "body" : "new 2",
      "user" : "3",
      "_id" : ObjectId("5215ddc11c18b90000000005"),
      "created" : ISODate("2013-08-22T09:45:37.903Z")
    }
  ]
}

user 3 のcomments.body が new 2 になってるのがわかる。なぜかというと、更新のほうで次のようなクエリが走るから。

// 更新のクエリ
posts.update(
  { _id: 5215ddc11c18b90000000002 },
  { '$set': { 'comments.1.body': 'new 2' } }
})

comments.1.body は findById で post を取得した時点では user 2 のデータを指してるんだけど、post を取得してから更新の処理が走る間に削除の処理が走るために user 3 のデータが書き換わってしまうというわけ。
これが Mongoose v3.x のバージョニングを使えば解決できる。v3.x では __v というフィールドを Mongoose が勝手に作って、配列の要素を削除や追加した場合に __v の値をインクリメントする。そして更新の際に __v の値を find の条件に入れるので、上記のスクリプトを実行すると、更新の処理がエラーになる。クエリはこんな感じで走る。

// 削除のクエリ
posts.update(
  {
    _id: ObjectId("5215ddde10f4fd0000000002"),
    __v: 0
  },
  {
    '$inc': { __v: 1 },
    '$set': { comments: ... }
  }
)

// 更新のクエリ
posts.update(
  {
    _id: ObjectId("5215ddde10f4fd0000000002"),
    __v: 0
  },
  { '$set': { 'comments.1.body': 'new 2' } }
)

更新のクエリはさっきと同じように comments.1.body を変更しようとしてるんだけど、検索部分に __v: 0 というのが追加されている。そしてその前の削除のクエリで '$inc': { __v: 1 } という処理を実行しているため、更新のクエリは条件にマッチせず、更新自体が行われない。

更新のほうのsaveメソッドでは次のようなエラーが発生する。

{ message: 'No matching document found.', name: 'VersionError' }

__v とかいうフィールドを持たせるのかなりダサイけどまあ現実的な解決方法なんだろうな、たぶん。

しかしトランザクションがないとこういう処理が大変なんだんなあ・・

要素の属性を全部取得する

例えば

<div data-foo="a" data-bar="b" data-baz="c">...</div>

みたいに任意のカスタムデータ属性がついてて、こっから

{ foo: 'a', bar: 'b', baz: 'c' }

みたいなデータを作りたいので属性を全部取得したいんだけど、 jQuery ではできないっぽい。

DOMの element.attributes を使えばいける。

var div = document.querySelector('div');
var attrs = div.attributes;
for (var i = 0, len = attrs.length; i < len; i++) {
  console.log(attrs[i].name); //=> data-foo, data-bar, data-baz
  console.log(attrs[i].value); //=> a, b, c
}

ほぼ使いどころないけど jQuery プラグインで書くんだったらこんな感じだろうか

$.fn.attrAll = function() {
  var ret = {};
  var attrs = this.get(0).attributes;
  var attr;
  for (var i = 0, len = attrs.length; i < len; i++) {
    attr = attrs[i];
    ret[attr.name] = attr.value;
  }
  return ret;
};
console.log( $('div').attrAll() ); // { data-foo: 'a', data-bar: 'b', data-baz: 'c' }

追記: data() でできた