前回のMithrilコードリーディングの続きです!前回はVirtual DOMのアルゴリズムの部分をメインで読んでいきました。今回はSPAのための軽量ライブラリとして活躍しているMithrilが持つrouteやDeferredの部分について解説です。コードを全部は掲載しないので、v0.2.0のコードを別で開いて読みながら参考までにコメントを見て進めていただければ。

ちなみに、ちょうど、前回のコードリーディングの記事の後くらいにMithrilの日本語本がオライリーから発売されたようです。

Mithril――最速クライアントサイドMVC
null


L522: m.trust()

htmlとして実行しても大丈夫であるstringに関してはm()によるvirtual DOM構築時にtrustを一度実行してあげると injectHTMLによってhtmlとしてDOMが構築されnodesのreferenceに追加されるようです。

使い方例:

var content = "<h1>Error: invalid user</h1>";

m.render("body", [
    m("div", m.trust(content))
]);

サーバー側からhtml文字列が返される場合などに使えそうですね。


L528 gettersetter

m.propから呼ばれるもので、定義時に渡された値の参照を持つクロージャーを返す関数です。返された関数が実行されると時は定義時の変数であるstoreを返します(getter)。引数付きで実行される場合はstoreの値を書き換えます(setter)。クロージャーを利用して非常に簡潔に実装されています。


L541: m.prop()

引数がthenを持つ関数、つまりPromise/Deferredである場合はpropifyを実行します(後述)。それ以外のケースではgettersetterを実行します。


L552: parameterize()

m.componentの実装の中身。第一引数にcomponentを受け取って、第二引数にm.componentで渡された追加引数がきます。controllerとviewをそれぞれwrapして、実行時に第二引数を追加する処理がされています。viewの場合は元々渡される引数であるctrlにconcatで引数を追加しています。そして、{controller: controller, view: view}の形式のオブジェクトをreturnします。


L565: m.component

上記のparameterize()を呼ぶだけです。


L568: m.mount()

m.moduleというのはcompatibilityのために残しているものだと思います。
mithrilの処理を開始する一番初めに呼ぶ関数です。DOMのrootとmithrilのcomponentを渡して、Virtual DOMの更新処理が開始されます。m.redraw.strategy(“all”) という記述は初めは必ずフルでVirtual DOMを実際のDOMに反映しなくてはいけないので、allがセットされています。以降diffというモードで振舞います。

onloadなどの前処理を行った後は、startComputation -> controllerのnew -> endFirstComputation と処理が行われます。後述のendFirstComputationで前回解説したbuildが呼ばれます。


L610: m.redraw()

m.redraw()は後述のendComputation()の中で呼ばれます。lastRedrawIdtという変数にrequestAnimationFrame(もしくは、setTimeout)の返り値を保存しておいて、RAFの場合はFRME_BUDGET後に、次に登場する内部関数のredraw()を呼びます。RAFがない環境ではsetTimeoutでFRME_BUDGET内に何度もcallされることを避けるために、現在時刻と前回を比較してFRME_BUDGETより大きければsetTimeoutで内部関数のredraw()を呼んでいます。
ちなみにこっちのredrawはm.redrawでアクセスできるので、Virtual DOMを意図的に更新する時に呼べる感じのようです。


L630: redraw()

内部関数のredrawでは実際にVirtual DOM diffが行われるrenderを呼びます。renderの前にcomputePreRedrawHookが登録されている場合は呼び出して、renderの後にcomputePostRedrawHookが登録されている場合は呼び出しています。
preの方はrouterによってページが切り替わった時のscrollの制御の処理などが登録されたりします。postの方はhistory.pushState用にdocument.titleをセットしたりします。


L652: m.startComputation()

ここがmithrilのミソかもしれませんが、startComputation()が呼ばれるたびに内部カウンタをincrementします。


L653: m.endComputation()

endComputation()ではstartComputation()でincrementしたカウンターをdecrementします。0に達した時点でredrawを呼びます。decrementした値が0以下の場合は0がセットされます。


L657: endFirstComputation()

最初の一回目か、DOMのイベントハンドラで呼ばれるautoredrawの時にはendComputationではなくendFirstComputationが呼ばれます。違いはm.redraw.strategy()が”none”の時のハンドリングが入っているくらいです。


L665: m.withAttr()

withAttrの使い方は公式ドキュメントにサンプルがあります。
https://lhorie.github.io/mithril/mithril.withAttr.html

上記のコードでwtihAttrにDOMの属性名とm.propにより作られるgettersetterを与えると実行時にeventのtargetをからpropの値を取得して、それを第2引数でもらっていたsetterに渡して実行します。


L676: m.route

前回のbackbone.jsコードリーディングの時にも出てきましたね。backboneと比べると少し簡易的な実装のように見えます。
見ていきます。

まずrouteの使い方に関しては公式ドキュメントをご参照ください。
https://lhorie.github.io/mithril/mithril.route.html

m.route(document.body, "/", {
    "/": Home,
    "/login": Login,
    "/dashboard": Dashboard,
});

このような引数を与えて、routingルールを定義できます。受け取ったらredirectという関数の中でrouteByValueを読んでmountします(後述)。そのredirectはroutingのモードに応じてwindowのonhashchange, onpopstateイベントリスナーの実行によって呼ばれます。

L703の部分では以下のような形式でconfigの指定からrouteが呼ばれた時に呼ばれます。どのようなrouteのmodeであっても、リンクを正しくroutingさせるためのコード(routeUnobtrusive)がonclick時に追加されるようです。

//Note that the '#' is not required in `href`, thanks to the `config` setting.
m("a[href='/dashboard/alicesmith']", {config: m.route});

L753: routeByValue

渡したpathと登録されているroute定義とを比較して実際にrouteを行う部分。routeの実態はm.mountを行っています。
まずは文字列マッチして、その次に正規表現のcheckをして、マッチした場合はrouteParamsとして処理します。


L790: routeUnobtrusive

リンク文字列形式でrouteを実行して実際にonclickが呼ばれた時に呼ばれるもの。元々のlinkの動作をpreventして、m.routeに切り替えて、通常のリンク形式からどのroute modeにも対応できるようにしているもの。


L804: buildQueryString & parseQueryString

よくありそうなutility的なやつ


L868: Deferred

ついに後半の山、Deferredにきました。
基本はPromises/A+に沿って実装しているもののようですが、2点ほど違うがあるとコメントに記述されています。
– thenのcallbackを同期的に呼ぶ (setTimeoutでasyncにすると遅すぎるから)
– Errorをthrowした時は、rejectionを実行するのではなく、bubble upする(理由は仕様がデフォルトのブラウザのエラーについて重要視しているものではないから)

使い方はこちらを参考に。
https://lhorie.github.io/mithril/mithril.deferred.html

この内部Deferredの使われ方ですが、m.deferredを呼び出した時に以下のように使われます。

    m.deferred = function () {
        var deferred = new Deferred();
        deferred.promise = propify(deferred.promise);
        return deferred
    };

    function propify(promise, initialValue) {
        var prop = m.prop(initialValue);
        promise.then(prop);
        prop.then = function(resolve, reject) {
            return propify(promise.then(resolve, reject), initialValue)
        };
        return prop
    }

m.deferredを呼び出した時に、初期値が空でpropifyを呼んでいます。

propifyの中ではm.propによってgetter/setterを生成し、それをオリジナルのpromiseのthenに登録します。

ここら辺結構不思議な感じなのでじっくり見ていきます。

promiseのthenにセットされたpropが今度はprop.thenを定義します。

呼び出し元のm.Deferredでdeferred.promiseに代入される予定の返り値はthenを持つpropになるので、m.deferredにより作られたpromiseのthen実行時にcallbackを登録すると、オリジナルのthenを再び呼び出して、その結果を同様にpropifyしたものをreturnしています。

オリジナルのthenがpropify実行時と、promiseとして返ったオブジェクトのthenの中でも再度呼ばれていますが、これは後述のDeferredのthenの中を見ると、nextというarrayにpushしています。なんでこんなことしているかというと、thenに渡すものを常にthenを持つpropに変換することで、propをthenに渡して登録した時にきちんとthenのチェーンにつなげられるという利点があるからだと思います。

例えば以下のような例を試すと、propであるusersにデータがセットされた上で次のthenのcallbackに値が引き継がれていきます。

var users = m.prop([]); //default value
var doSomething = function(usersData) {
    console.log(users()) // -> users data
}

m.request({method: "GET", url: "./users"}).then(users).then(doSomething)


L868 Deferred

Deferredの中ではまずはpromiseオブジェクトを生成します。(ちなみにDeferredはnewを使って呼び出されるものなので、毎回promiseは別オブジェクトになります)

resolveでは初めて呼ばれた時に飲のみstateにはRESOLVINGをセットして、fireを呼びます。
rejectでは初めて呼ばれた時に飲のみstateにはREJECTINGをセットして、fireを呼びます。

thenは新しくDeferredをnewして、もし自身が既にresolveされていたら新しく生成したdeferredのresolveを呼びます(rejectも同様)。例えば、既にxhrによってloadされた後にthenを呼び出すと即時に呼ばれるようにするための実装ですね。
もし、まだ自身がresolved/rejected状態になっていなければnextにpushして解決時にまとめて解決するチェーンに追加します。

次はresolve/rejectで呼んでいるfireです。promiseValue(resolve/reject実行時の引数)がthenである場合はthennableの中でthenを実行します。(あまりresolve実行時の引数をdeferredにする例はみたことないような気がします)その他のケースではDeferredをnewした時の引数であるsuccessCallback(基本はthenが実行されてdeferredがnewされた時にセットされたものだと思います)を呼んでfinishします。finishでは前に先ほど出てきたnextの中に次があればそれぞれresolveを呼びます。


L992: m.sync

よくあるDeferredのwhen / promise allの実装。
なるほど。ただ、synchronizerをthenにセットして、本来のthenに渡ってくる値をresultsにキャッシュしながら、deferredの数をカウントダウンして、0になったら新しく生成したdeferredのresolve/rejectが呼ばれるようになっています。また、一個でもrejectがあれば、syncが生成したpromiseはrejectになるようです。


L1020: ajax

ここは正直普通のajaxの実装なのかなと思います。jsonp対応をして、それ以外はXMLHttpRequestをnewします。


L1111: m.request

requestでは先ほどのajaxを実行しつつ、生成したdeferredを結果に応じてresolve/rejectします。想像通りの実装かなと思います。


以上、少々長かったですが、Mithrilコードリーディング後編終了です!Virtual DOMの実装がもちろん重要ですが、SPA開発を効率よく行うところにfocusされているため、Deferredやajaxやroutingがきちんと実装されています。

前半のVirtualDOMの実装は”なるほど”という内容でした。
後半はBackbone.jsなどを読んでいれば、すんなり読めると思いますが、Deferredの実装などは思った以上に読み応えがあり、学びもありました。

個人的にコードリーディングしてよかったのと思った点は、Mithrilのように有名になるライブラリの実装は、魔法でもなんでもなく意外と普通(普通というと語弊がありそうですが)なんだなーと感じたのが収穫です。

引き続きオープンソースのコードリーディング会を続けていきます。
さっそく来週は社内でunderscore.jsリーディング会をする予定です。


Related Posts

JS

非同期処理でのwindow.openはポップアップブロックされる

実はあまり知らなかったですがユーザーアクション(onClick等)の処理の中にajaxやsetTimeoutによる非同期な処理があるとwindow.open()によるpopup blockが起きてしまいます。 何故かブロックされるケースとされないケースがあったので、初めはクロスドメインだとブロックされるのかなと思ったりしていましたが関係なかったようです。 以下、詳細と回避方法。

JS

Yeomanを使ってWebアプリ開発の雛形を簡単作成

Yeoman http://yeoman.io/ まず使うにはnode.jsがインストールされているのが前提。 Yeoman本体をインストール npm install -g yo 一番標準のwebapp generatorをグローバルにインストール npm install -g generator-webapp 任意のディレクトリ(例えば/Users/tejitak/workspace/Test)を作りその下で以下のコマンド yo webapp するとGruntのファイルや/appなどweb applicationの雛形ができあがる。 試しにサーバーを起動してみると既にlocalhost:9000にアクセスして正しくindex.htmlがデプロイされていることを確認できる。 自分はrequiresなどを使ってAMDでアプリを構築する場合が多かったので、試しにAMD用のwebapp generatorを使ってみる。

JS

node expressサーバーの起動をgruntから行う

node+express環境ではnode appでサーバー起動をする方法はdeprecatedで最近はnpm startコマンドが推奨されているみたいです。 npm startを発行するとpackage.jsonの中のscriptsに記載されたコマンドが発行され、通常express環境なら/bin/wwwというスクリプトが叩かれます。 package.json wwwもjavascriptファイルで、中ではapp.jsを特定のポートに対してlistenしているだけです。 ただ、もしサーバーの起動をgruntに任せたい場合はどうしましょう。