ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Object.observe()とNode.jsのイベントループの関係

1. はじめに

最近 Chrome で Object.observe() がデフォルトで有効になりました。

コミットログを見ればわかりますが、2回 revert された後の3度目の正直のコミットです。

確かに Object.observe() は非常に強力なAPIですけど、ES6の仕様候補の機能に入っていません。(ES.harmony or ES7?) ちょっと前に大幅な仕様見直しがあったので、こんな苦労してまでよくデフォルトで有効にするなぁと不思議でした。

しかし今日 AngularJS 2.0 のリリースを見て合点いきました。PolymerのMLにも流れてました。

Googleさん、攻めてますねー。

Object.observe()の機能については下記記事を参照にしてください。(また js-nextさんの記事の紹介だ、いつもありがとうございます。)

仕様改定前の古いものですがいちおう私のエントリーも載せておきます。

そこで気になるNodeとの関係です。デフォルトで Object.observe()が使えるのは、最新(breeding edge)版のV8 3.25なので、昨日紹介した Node(V8 3.24)では、依然 harmony オプションが必要です。ただこのまま Node-v0.12 のリリースが遅れると、ひょっとして Node の V8 が3.25にアップデートするかもしれませんし、次の Node-v0.13(もしかして1.x?)では確実に入るでしょう。近い将来、NodeでObject.observe() がデフォルトで使えるとなると、これまでと全く違う世界が現れるのではないかと夢がひろがりんぐしてしまいます。

個人的には、まだ具体的に夢のあるグッとくるような使い方は考えられていませんが、NodeでObject.observe()を使うと、そのコールバックってNodeのコールバックとどう違うんだろう、それともどう一緒なんだろうか? といった疑問がふとわいてきました。そう思うと気になって気になって仕方がなく、我慢できずにさっそく検証してみました。(原稿の締め切り直前にやってていいんだろうか・・・・)

2. Node では Object.observe() のコールバックはどのタイミング実行される?

V8の最新版の実装を見てみると Object.observe() は内部に Microtask Queue を保持して、タスク管理をしているようです。昨日紹介した Promise も同じ仕組みみたいです。

Nodeで扱うコールバックは、I/Oコールバック、setImmediate、0秒の setTimeout、process.nextTick、EventEmitterとざっと5種類。これに Object.observeのコールバック、Object.deliverChangeRecordsによるコールバックの2種類で合計7種類あります。
EventEmitter と Object.deliverChangeRecords は同期的な動作だと予想されるので2つのEventEmitterの間に Object.deliverChangeRecords を挟み込みます。結果、コールバックの起動順序を表示するテストスクリプトを以下の様にしました。

// Object.observe()とNodeのコールバックの実行順を確認するテスト
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var fs = require('fs');
var order = 1;
// Test Event Object
function MyObj() {}
util.inherits(MyObj, EventEmitter);
var myobj = new MyObj();
// Event emitter callback
myobj.on('myevent', function(msg) {
  console.log(order++, 'myevent msg: '+ msg +' emitted');
});
// Object.observe callback
function callback(changeRecord) {
  console.log(order++, 'Object.observed:', changeRecord);
}
Object.observe(myobj, callback);

// Start Testing
fs.stat(__filename, function(stat) {
  console.log(order++, 'I/O callback(fs.stat)');
});
// At the last of event loop
setImmediate(function() {
  console.log(order++, 'setImmediate called');
});
// Timer
setTimeout(function() {
  console.log(order++, 'setTimeout 0sec called');
}, 0);
// At the finish of JavaScript Runtime
process.nextTick(function(){
  console.log(order++, 'process.nextTick called');
});

myobj.hoge = 'deliverChanged';
myobj.emit('myevent', 'first event');
Object.deliverChangeRecords(callback);
myobj.emit('myevent', 'second event');
myobj.hoge = 'foo';

で、実行結果がこれです。

$ node --harmony test1.js
1 'myevent msg: first event emitted'
2 'Object.observed:' [ { type: 'add',
    object: { _events: [Object], hoge: 'deliverChanged' },
    name: 'hoge' } ]
3 'myevent msg: second event emitted'
4 'process.nextTick called'
5 'Object.observed:' [ { type: 'update',
    object: { _events: [Object], hoge: 'foo' },
    name: 'hoge',
    oldValue: 'deliverChanged' } ]
6 'setTimeout 0sec called'
7 'I/O callback(fs.stat)'
8 'setImmediate called'

結果を表にまとめるとこんな感じです。

機能 記述順 実行順
I/O callback(fs.stat) 1 7
setImmediate 2 8
setTimeout 3 6
process.nextTick 4 4
event emitter #1 5 1
Object.deliverChangeRecords 6 2
event emitter #2 7 3
Observed Object property の変更 8 5

3. コールバック起動の順番とイベントループの関係

個人的に予想したのは Object.observe()のコールバックの後に process.nextTickかなと思ったのですが違ってました。ソースを確認すると、メインモジュールの呼び出し・実行の最後に process.nextTick を呼び出していました。
https://github.com/joyent/node/blob/master/lib/module.js#L491-L492
また、Object.observe() のコールバックは JS の実行(Run)の最後に行われるので、メインモジュール実行完了後に起動されます。
図にするとこんな感じです。

まとめると、

  1. EventEmitter と Object.deliverChangeRecords() は、JSの実行中に同期的にコールバックが起動されるので書いた順番に依存します。テストスクリプトの出力は、イベントループの図「0:Load」で起動しています。
  2. process.nextTickはメインモジュールの読み込み・実行の最後やNodeのコールバック処理の最後で呼び出されるため、V8実行の最後の Object.observe()よりも前に起動される。
  3. その他のコールバック(Timer, SetImmediate, I/O)は libuv で実装されたイベントループの順番で起動される。

ということです。

あぁ、すっきりした。