2017-08-03

ActiveSupport.on_loadとrun_load_hooksのコードリーディング

ActiveSupport.onload と ActiveSupport.run_load_hooks のコードを読んでみました。

両メソッドともにActionSupport::LazyLoadHooksのモジュールで定義されているメソッドになります。on_loadを呼び出すと@load_hooks[name]にProcが格納されます。run_load_hooksを呼び出すと@loaded[name]にbase(Procの呼び出し引数 or instance_evalのコンテキスト)を格納するとともに、@load_hooks[name]のブロックをbaseのコンテキストで呼び出します。

module ActiveSupport
  module LazyLoadHooks
    def self.extended(base) # :nodoc:
      base.class_eval do
        @load_hooks = Hash.new { |h, k| h[k] = [] }
        @loaded     = Hash.new { |h, k| h[k] = [] }
      end
    end

    def on_load(name, options = {}, &block)
      @loaded[name].each do |base|
        execute_hook(base, options, block)
      end

      @load_hooks[name] << [block, options]
    end

    def execute_hook(base, options, block)
      if options[:yield]
        block.call(base)
      else
        base.instance_eval(&block)
      end
    end

    def run_load_hooks(name, base = Object)
      @loaded[name] << base
      @load_hooks[name].each do |hook, options|
        execute_hook(base, options, hook)
      end
    end
  end

ActionMailerの例だと、ActionMailer::Baseの最後の行でActiveSupport.run_load_hooksを呼び出しています。これはActionMailer::Baseのmix-inやメソッドの定義などが全て終わったタイミングで、hookの処理を呼び出すためです。

module ActionMailer
  class Base < AbstractController::Base
    include DeliveryMethods
...
      ActiveSupport.run_load_hooks(:action_mailer, self)
  end
end

RailtieなどでRailsのモジュールを拡張する場合にも使われ、以下のように対象のクラスがロードされたタイミングでクラスを拡張できます。メソッドなどがロードされた状態なのでmethod missingにもならないし、もちろんクラス自体も存在します。

require 'http_action_mailer/delivery_method'

module HttpActionMailer
  class Railtie < Rails::Railtie
    initializer 'http_action_mailer.add_delivery_method', before: 'action_mailer.set_configs' do
      ActiveSupport.on_load :action_mailer do
        ActionMailer::Base.add_delivery_method(:http, HttpActionMailer::DeliveryMethod)
      end
    end
  end
end
このエントリーをはてなブックマークに追加