※ RubyやRailsの素人なので,用語や表現など間違っているところがあったら,指摘してもらえば幸いです!
はじめに
コントローラで生成されたインスタンス変数はビュー側でも使えることは,ある意味当たり前かも知れないけど,Railsの中ではどういう処理が行われているのか気になった.
ビュー側でコントローラ側で生成されたすべてのインスタンス変数にアクセスできることから,
- コントローラのアクションメソッドの実行後
- レンダラーがビューをレンダリングする前
のタイミングで,インスタンス変数の情報を渡すような処理をしているのではないかと推測しつつ,Railsのコードを読んでみた.
環境
- Ruby 2.2.3
- Rails 4.1.13
- RubyMine 8.0.3
1. コントローラのアクションメソッドはどこから呼び出されているのか
コントローラのアクションメソッドへのエントリポイント
結論からいうと,ActionController::ImplicitRender#send_action
で,super
により呼び出されていた.
module ActionController
module ImplicitRender
def send_action(method, *args)
ret = super
default_render unless performed?
ret
end
def default_render(*args)
render(*args)
end
end
end
ところが,なぜActionController::ImplicitRender#send_action
のスーパーメソッドがアクションメソッドになっているんだろう.
ActionController::ImplicitRender#send_actionのスーパメソッドがアクションメソッドになっている理由
まずは,継承関係を調べてみよう.
-
ActionController::ImplicitRender
は,ActionController::Base
にmix-inされている.
module ActionController
class Base < Metal
MODULES = [
...
ImplicitRender,
...
]
MODULES.each do |mod|
include mod
end
end
end
-
ActionController::Base
は,ActionController::Metal
を継承している. -
ActionController::Metal
は,AbstractController::Base
を継承している.
module ActionController
class Metal < AbstractController::Base
end
end
-
AbstractController::Base
の中で,手がかりを見つけた.
module AbstractController
class Base
class << self
private
def process_action(method_name, *args)
send_action(method_name, *args)
end
alias send_action send
end
ActionController::ImplicitRender#send_action
のスーパメソッドは,AbstractController::Base#send_action
で,その正体は他ではなく,send
であった.
つまり,ActionController::ImplicitRender#send_action
の中のret = super
の実体はret = send(method, *args)
で,コントローラのアクションメソッドが呼び出されていたということかな.
2. レンダラーはいつビューをレンダリングするのか
コントローラのアクションメソッドの中で,以下のようにrender
メソッドを明示的に呼び出している場合,呼び出していない場合がある.
class UsersController < ApplicationController
def fuga
# 明示的に呼び出していない場合
end
def hoge
# 明示的に呼び出している場合
render :fuga
end
end
-
render
メソッドを明示的に呼び出していない場合 -
コントローラのアクションメソッドの処理が終わった時,
response_body
が生成されてないため,performed?
はfalse
1 -
ActionController::ImplicitRender#send_action
からActionController::ImplicitRender#default_render
が呼び出される. -
ActionController::ImplicitRender#default_render
からActionController::Instrumentation#render
が呼び出される -
render
メソッドを明示的に呼び出している場合 -
コントローラのアクションメソッドの中で,
ActionController::Instrumentation#render
が呼び出される -
コントローラのアクションメソッドの処理が終わった時,
response_body
が生成されてないため,performed?
はtrue
-
ActionController::ImplicitRender#send_action
からActionController::ImplicitRender#default_render
が呼び出されない.
ちなみに,ActionController::ImplicitRender#send_action
の中のperformed?
メソッドは,ActionController::Metal#performed?
で,レスポンスが生成されているのか否かを判断する.
module ActionController
class Metal
def performed?
response_body || (response && response.committed?)
end
end
end
さて,どの場合からも共通しているActionController::Instrumentation#render
から探ってみよう.
ActionController::Instrumentation#render
スーパーメソッドActionController::Rendering#render
が呼び出される.
module ActionController
module Instrumentation
def render(*args)
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
end
render_output
end
end
end
ActionController::Rendering#render
- 既に
response_body
が生成されていたら,DoubleRenderError
を発生させる. - そうでない場合は,スーパーメソッド
AbstractController::Rendering#render
が呼び出される.
module ActionController
module Rendering
def render(*args) #:nodoc:
raise ::AbstractController::DoubleRenderError if self.response_body
super
end
end
end
AbstractController::Rendering#render
response_body
を生成するところは,ActionController::Renderers#render_to_body
であった.
module AbstractController
module Rendering
def render(*args, &block)
options = _normalize_render(*args, &block)
self.response_body = render_to_body(options)
_process_format(rendered_format, options) if rendered_format
self.response_body
end
end
end
ActionController::Renderers#render_to_body
- コンテンツタイプが
json
,js
,xml
の場合は,_handle_render_options
により処理されるらしい. - そうでない場合は,スーパーメソッド
ActionController::Rendering#render_to_body
が呼び出される.
module ActionController
module Renderers
def render_to_body(options)
_handle_render_options(options) || super
end
def _handle_render_options(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
return send("_render_option_#{name}", options.delete(name), options)
end
end
nil
end
end
end
ActionController::Rendering#render_to_body
スーパーメソッドActionView::Rendering#render_to_body
が呼び出される.
module ActionController
module Rendering
def render_to_body(options = {})
super || _render_in_priorities(options) || ' '
end
end
end
ActionView::Rendering#render_to_body
ActionView::Rendering#_render_template
が呼び出される.
module ActionView
module Rendering
def render_to_body(options = {})
_process_options(options)
_render_template(options)
end
end
end
ActionView::Rendering#_render_template
-
view_renderer
は,ActionView::Renderer
のインスタンスを一回だけ生成して返す. -
view_context
は,ActionView::Base
のインスタンスを毎回生成して返す.
つまり,ここがActionView::Renderer#render
へのエントリーポイントのようだ.
module ActionView
module Rendering
module ClassMethods
def view_context_class
@view_context_class ||= begin
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
include routes.url_helpers
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
end
end
attr_internal_writer :view_context_class
def view_context_class
@_view_context_class ||= self.class.view_context_class
end
def view_context
view_context_class.new(view_renderer, view_assigns, self)
end
def view_renderer
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
private
def _render_template(options) #:nodoc:
variant = options[:variant]
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
view_renderer.render(view_context, options)
end
end
end
3. いつインスタンス変数が渡されるのか
ActionView::Rendering#view_context
で呼び出しているメソッドview_assigns
はAbstractController::Rendering#view_assigns
だった.
AbstractController::Rendering#view_assigns
ここで,コントローラで生成されたすべてのインスタンス変数(保護されている変数は除く)の情報を返すメソッドだった.
module AbstractController
module Rendering
DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
@_action_name @_response_body @_formats @_prefixes @_config
@_view_context_class @_view_renderer @_lookup_context
@_routes @_db_runtime
).map(&:to_sym)
def view_assigns
protected_vars = _protected_ivars
variables = instance_variables
variables.reject! { |s| protected_vars.include? s }
variables.each_with_object({}) { |name, hash|
hash[name.slice(1, name.length)] = instance_variable_get(name)
}
end
def _protected_ivars # :nodoc:
DEFAULT_PROTECTED_INSTANCE_VARIABLES
end
end
end
ということは,これをパラメータとするコンストラクターを持つActionView::Base
が怪しかったので,見てみた.
ActionView::Base#initialize
ActionView::Base#initialize
のassigns
パラメータは,ActionView::Base#assign
によりインスタンス変数としてセットされる!
module ActionView
class Base
def assign(new_assigns) # :nodoc:
@_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
if context.is_a?(ActionView::Renderer)
@view_renderer = context
else
lookup_context = context.is_a?(ActionView::LookupContext) ?
context : ActionView::LookupContext.new(context)
lookup_context.formats = formats if formats
lookup_context.prefixes = controller._prefixes if controller
@view_renderer = ActionView::Renderer.new(lookup_context)
end
assign(assigns)
assign_controller(controller)
_prepare_context
end
end
end
まとめ
- コントローラのメソッドは``ActionController::ImplicitRender#send_action`で実行される.
- レンダラーは,
ActionView::Renderer#render
にてビューをレンダリングする. - コントローラで生成されたインスタンス変数の情報は,
AbstractController::Rendering#view_assigns
からゲットできる. -
ActionView::Base#assign
により,ビューにインスタンス変数の情報をセットできる.
脚注
-
厳密にいうと,
render
メソッドを呼び出していなくても,redirect_to
メソッドを呼び出した場合もresponse_body
は生成されて,performed?
はfalse
になる. ↩