LoginSignup
16
17

More than 5 years have passed since last update.

Rails4はどのようにコントローラのインスタンス変数をビューに渡しているのか

Last updated at Posted at 2016-03-02

※ RubyやRailsの素人なので,用語や表現など間違っているところがあったら,指摘してもらえば幸いです!

はじめに

コントローラで生成されたインスタンス変数はビュー側でも使えることは,ある意味当たり前かも知れないけど,Railsの中ではどういう処理が行われているのか気になった.

ビュー側でコントローラ側で生成されたすべてのインスタンス変数にアクセスできることから,

  • コントローラのアクションメソッドの実行後
  • レンダラーがビューをレンダリングする前

のタイミングで,インスタンス変数の情報を渡すような処理をしているのではないかと推測しつつ,Railsのコードを読んでみた.

環境

  • Ruby 2.2.3
  • Rails 4.1.13
  • RubyMine 8.0.3

1. コントローラのアクションメソッドはどこから呼び出されているのか

コントローラのアクションメソッドへのエントリポイント

結論からいうと,ActionController::ImplicitRender#send_actionで,superにより呼び出されていた.

gems/actionpack/lib/action_controller/metal/implicit_render.rb
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されている.
gems/actionpack/lib/action_controller/base.rb
module ActionController
  class Base < Metal
    MODULES = [
      ...
      ImplicitRender,
      ...
    ]

    MODULES.each do |mod|
      include mod
    end
  end
end
  • ActionController::Baseは, ActionController::Metalを継承している.
  • ActionController::Metalは,AbstractController::Baseを継承している.
gems/actionpack/lib/action_controller/metal.rb
module ActionController
  class Metal < AbstractController::Base
  end
end
  • AbstractController::Baseの中で,手がかりを見つけた.
gems/actionpack/lib/abstract_controller/base.rb
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メソッドを明示的に呼び出している場合,呼び出していない場合がある.

app/controllers/users_controller.rb
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?で,レスポンスが生成されているのか否かを判断する.

gems/actionpack/lib/action_controller/metal.rb
module ActionController
  class Metal
    def performed?
      response_body || (response && response.committed?)
    end
  end
end

さて,どの場合からも共通しているActionController::Instrumentation#renderから探ってみよう.

ActionController::Instrumentation#render

スーパーメソッドActionController::Rendering#renderが呼び出される.

gems/actionpack/lib/action_controller/metal/instrumentation.rb
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が呼び出される.
gems/actionpack/lib/action_controller/metal/rendering.rb
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であった.

gems/actionpack/lib/abstract_controller/rendering.rb
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が呼び出される.
gems/actionpack/lib/action_controller/metal/renderers.rb
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が呼び出される.

gems/actionpack/lib/action_controller/metal/rendering.rb
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が呼び出される.

gems/actionview/lib/action_view/rendering.rb
module ActionView
  module Rendering
    def render_to_body(options = {})
      _process_options(options)
      _render_template(options)
    end  
  end
end

ActionView::Rendering#_render_template

  1. view_rendererは,ActionView::Rendererのインスタンスを一回だけ生成して返す.
  2. view_contextは,ActionView::Baseのインスタンスを毎回生成して返す.

つまり,ここがActionView::Renderer#renderへのエントリーポイントのようだ.

gems/actionview/lib/action_view/rendering.rb
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_assignsAbstractController::Rendering#view_assignsだった.

AbstractController::Rendering#view_assigns

ここで,コントローラで生成されたすべてのインスタンス変数(保護されている変数は除く)の情報を返すメソッドだった.

gems/actionpack/lib/abstract_controller/rendering.rb
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#initializeassignsパラメータは,ActionView::Base#assignによりインスタンス変数としてセットされる!

gems/actionview/lib/action_view/base.rb
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により,ビューにインスタンス変数の情報をセットできる.

脚注


  1. 厳密にいうと,renderメソッドを呼び出していなくても,redirect_toメソッドを呼び出した場合もresponse_bodyは生成されて,performed?falseになる. 

16
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
17