Elmのアプリケーションコードを読んでみよう
ElmアプリケーションはElmアーキテクチャーに沿って構築していきます。構築したアプリケーションコードはElmのコアコード上でアプリケーションとして振る舞います。
Elmのコアコードがどんなことをしているのか気になったので、今回はElmアプリケーションが起動するまでにどういうことをやっているのかを追っていきたいと思います。
起動呼び出し
Elmアプリケーションを起動するには以下の呼び出しを最初に行います。
var app = Elm.Main.fullscreen()
他にもいろいろ起動するために呼び出せる関数はありますが、とりあえずこちらから考えます。見て分かる通り Elm.Main
オブジェクトを操作しているので、まずはこれがどういうオブジェクトなのか知ることから始めていきましょう。
Elm.Main
var Elm = {}; Elm['Main'] = Elm['Main'] || {}; if (typeof _user$project$Main$main !== 'undefined') { _user$project$Main$main(Elm['Main'], 'Main', undefined); }
非常にシンプルなコードですね。_user$project$Main$main
をみると、どうやらコンパイラは モジュール名$関数名
という名前付けをしているようです。
Elm['Main']
にどういう変更が加えられるのか見ていきましょう。
makeProgram
function makeProgram(flagChecker) { return F2(function(debugWrap, impl) { return function(flagDecoder) { return function(object, moduleName, debugMetadata) // == _user$project$Main$main { var checker = flagChecker(flagDecoder, moduleName); if (typeof debugMetadata === 'undefined') { normalSetup(impl, object, moduleName, checker); } else { debugSetup(A2(debugWrap, debugMetadata, impl), object, moduleName, checker); } }; }; }); }
_user$project$Main$main
は object, moduleName, debugMetadata
を引数に取る無名関数みたいです。flagChecker
は checkNoFlags
と checkYesFlags
が用意されています。
F2
は関数を受け取ってカリー化して返します。逆に A2
はカリー化された関数に引数を渡して評価する関数です。 debugWrap
は _elm_lang$virtual_dom$VirtualDom_Debug$wrap
です。
impl
は Html.program
に渡すおなじみのレコードです。見てわかるように Elm['Main']
は debugMetadata
の有無で呼び出す関数が異なります。
今回は undefined
でした。Html.program
はこの関数の実行結果で実装が定義されます。
normalSetup
function normalSetup(impl, object, moduleName, flagChecker) { object['embed'] = function embed(node, flags) { while (node.lastChild) { node.removeChild(node.lastChild); } return _elm_lang$core$Native_Platform.initialize( flagChecker(impl.init, flags, node), impl.update, impl.subscriptions, normalRenderer(node, impl.view) ); }; object['fullscreen'] = function fullscreen(flags) { return _elm_lang$core$Native_Platform.initialize( flagChecker(impl.init, flags, document.body), impl.update, impl.subscriptions, normalRenderer(document.body, impl.view) ); }; }
ここで Elm['Main']
に変更が加えられます。これ以上、 Elm['Main']
に変更は加えてなさそうです。
initialize
function initialize(init, update, subscriptions, renderer) { // ambient state var managers = {}; var updateView; // init and update state in main process var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { var model = init._0; updateView = renderer(enqueue, model); var cmds = init._1; var subs = subscriptions(model); dispatchEffects(managers, cmds, subs); callback(_elm_lang$core$Native_Scheduler.succeed(model)); }); function onMessage(msg, model) { return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { var results = A2(update, msg, model); model = results._0; updateView(model); var cmds = results._1; var subs = subscriptions(model); dispatchEffects(managers, cmds, subs); callback(_elm_lang$core$Native_Scheduler.succeed(model)); }); } var mainProcess = spawnLoop(initApp, onMessage); function enqueue(msg) { _elm_lang$core$Native_Scheduler.rawSend(mainProcess, msg); } var ports = setupEffects(managers, enqueue); return ports ? { ports: ports } : {}; }
続けて fullscreen
呼び出しによる起動に至るまでの処理を眺めていきます。この関数はかなり重要そうです。
_elm_lang$core$Native_Scheduler.nativeBinding
に渡されている関数はどちらも同じような内容です。おそらく view
を更新して effectManager
に cmds
と subs
を渡して変更を通知しています。
_elm_lang$core$Native_Scheduler.nativeBinding
を始めとする _elm_lang$core$Native_Scheduler
モジュールの関数はElmアプリケーションのイベントループに大いに関係しています。
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
nativeBinding
が返すオブジェクトは後にenqueueされて処理されます。initApp
は初期化に必要な処理、onMessage
はメッセージを受け取った時に呼び出す処理だと推測できます。
setupEffects
は dispatchEffects
のための準備ではないでしょうか。もしかするとEffectManager関係の処理かもしれないです。ここで返しているオブジェクトは app
に代入されますが利用してないですね。
spawnLoop
function spawnLoop(init, onMessage) { var andThen = _elm_lang$core$Native_Scheduler.andThen; function loop(state) { var handleMsg = _elm_lang$core$Native_Scheduler.receive(function(msg) { return onMessage(msg, state); }); return A2(andThen, loop, handleMsg); } var task = A2(andThen, loop, init); return _elm_lang$core$Native_Scheduler.rawSpawn(task); }
一見するとこんがらがりそうなんですが、return
文に注目すると task
を enqueue
しているのがわかります。
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
この task
は _elm_lang$core$Native_Scheduler.nativeBinding
の仲間です。これは _elm_lang$core$Native_Scheduler.andThen
で処理がラップされています。
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
コールバック関数の loop
は handleMsg
という task
を設定して再度 andThen
でラップしています。_elm_lang$core$Native_Scheduler.receive
も _elm_lang$core$Native_Scheduler.nativeBinding
の仲間です。
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
rawSpawn
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
process
で task
をラップしています。 process
を enqueue
します。
work
キューに溜まった process
を処理します。取り出して process.root
があれば step
関数に渡します。process
がなくなると活動を停止します。
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
step
enqueue
された process
は step
関数で処理されます
core/Scheduler.js at 5.1.1 · elm-lang/core · GitHub
enqueue
された task
をおさらいします。
process(andThen(loop, nativeBinding(task)))
順に処理していきますが、 loop
が実行される気配がありません。 loop
は process.stack.callback
に積まれたままです。
ちょっと謎ですね。 loop
はいつ呼ばれるのでしょうか。
nativeBinding(task)
の処理では登録していた関数を呼び出しています。ここで呼び出しているのは初期化処理をする関数です。
var initApp = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { var model = init._0; updateView = renderer(enqueue, model); var cmds = init._1; var subs = subscriptions(model); dispatchEffects(managers, cmds, subs); callback(_elm_lang$core$Native_Scheduler.succeed(model)); });
renderer
は normalRenderer(document.body, impl.view)
の実行結果です。
function normalRenderer(parentNode, view) { return function(tagger, initialModel) { var eventNode = { tagger: tagger, parent: undefined }; var initialVirtualNode = view(initialModel); var domNode = render(initialVirtualNode, eventNode); parentNode.appendChild(domNode); return makeStepper(domNode, view, initialVirtualNode, eventNode); }; }
normalRenderer
では初期DOMの生成を行い、stepper
関数の作成を行います。
function makeStepper(domNode, view, initialVirtualNode, eventNode) { var state = 'NO_REQUEST'; var currNode = initialVirtualNode; var nextModel; function updateIfNeeded() { switch (state) { case 'NO_REQUEST': throw new Error( 'Unexpected draw callback.\n' + 'Please report this to <https://github.com/elm-lang/virtual-dom/issues>.' ); case 'PENDING_REQUEST': rAF(updateIfNeeded); state = 'EXTRA_REQUEST'; var nextNode = view(nextModel); var patches = diff(currNode, nextNode); domNode = applyPatches(domNode, currNode, patches, eventNode); currNode = nextNode; return; case 'EXTRA_REQUEST': state = 'NO_REQUEST'; return; } } return function stepper(model) { if (state === 'NO_REQUEST') { rAF(updateIfNeeded); } state = 'PENDING_REQUEST'; nextModel = model; }; }
makeStepper
は 1フレームごとに実行される関数の登録を行う関数 stepper
の作成を行います。virtualDOMの振る舞いを記述しているのもここになります。DOM差分を計算して適用している様子がわかります。
applyPatches
では差分適用のほかイベントに対する関数の登録も行っています。renderer
によって作成された関数 updateView
は onMessage
関数の中で利用されています。
dispatchEffects
では特に何もしないため最後にコールバック関数を呼び出します。
model
を succeed
でラップしてものをコールバック関数は受け取り新たな process.root
としています。同時に process
を enqueue
しています。
succeed
は step
関数で処理されます。process.stack.callback
に登録した loop
関数がようやく実行されます。
が、特に何事もなく process
は処理されて初期化処理は終了します。
まとめ
Elmアプリケーションの初期化処理でした。今回の重要登場人物は次のとおりです。
makeProgram
... デバッグの有無を判定normalSetup
...fullscreen
,embed
を定義initialize
... 初期化タスク定義と初期化実行spawnLoop
... 初期化タスクの合成rawSpawn
... processでラップ、キューにタスクを登録work
... キューに溜まったprocess
を処理step
... タスク処理makeStepper
... DOMの差分処理
流れもざっくり振り返りましょう。
Elmオブジェクト準備
- オブジェクトを作る
normalSetup
を呼んでfullscreen
を定義する
初期化する
initialize
を呼ぶspawnLoop
で初期化タスクを作成rawSpawn
を呼び、キューに追加work
を呼び初期化タスクを順番に実行step
を呼びタスクを処理、process.root
を設定initApp
に登録された初期化処理を実行- 初期viewの追加