Railsのviewとlayoutの評価順序はview => layoutとなっており、viewで定義したcontext_forやprovideはlayout側でyieldすることで利用できるようになっていたり、layout側で引数無しのyieldをすれば評価後のviewのレンダリングが出来るようになっています。

今回、この挙動をコードレベルで理解したかったので関連する部分のコードリーディングをしてみました。

Railsのバージョンは5.0.1です。

コードリーディング

レンダリング時にはActionView::TemplateRenderer.new(@lookup_context).render(context, options)が呼び出されます。

render_template、render_with_layoutメソッドが肝になります。

render_with_layoutのyield(layout)でブロックの内容を評価していますが、このブロックはview部分のレンダリングになります。引数のlayoutはActionView::Templateのインスタンスになります。ActionView::Template#renderは以下のように定義されています。

compile!メソッドが重要で、HTMLを出力するメソッドを動的に定義しています。

ここが動的に定義している部分↓#{code}で色々やってます。

実際にはこのようなメソッドが定義されます。

<%= %>で書いたものはappend、HTMLで書いたものはsafe_appendされるように変換されている感じがします(補足の項を参照)

view.sendでは定義した動的メソッドを呼び出します。viewやlayoutにyieldが含まれていればブロック内の{ |*name| view._layout_for(*name) }が評価されます。この時点でview部分のレンダリングが完了しています。

layoutの評価箇所を再掲します↓

layoutがあればview.view_flow.set(:layout, content)が呼ばれます。

getは@contentからコンテンツを取得するメソッドになっています。layoutファイル内でyieldを実行するとブロックが評価されることになり、view._layout_for(*name)が呼ばれます。

指定がなければ:layout、つまりviewでレンダリングした部分が呼ばれます。指定があればcontent_forやprovideで定義したコンテンツが返されます。これがerbで使っているyieldの正体です。

ちなみにcontent_forはこんな感じで、yieldした結果を@view_flow.appendしています。

provideも@view_flow.appendでコンテンツを追加しています。

ということでview => layoutの順に評価され、評価されたviewはcontent_forやprovideで定義したコンテンツと同様のハッシュの中に格納され、layoutファイル内ではyieldでコンテンツを取得している、ということになります。

補足

動的メソッドのコード内にあるappendとsafe_appendですが、ActionView::Template::Handlers::Erubisクラスで定義されていました。