Rails Rails 源码分析之 eager_load! 篇

seaify · 发布于 2015年08月10日 · 3284 次阅读
本帖已被设为精华帖!

eager_load!

在rails server执行时,有的文件会被提前加载进来,所以这些文件中的class就已经被定义了,而有的class,实际上,是在第一次被使用的时候,才会去根据class名,在目录中,去定位到文件,然后读取加载进来。

造成上述差别的,就是eager_load!, 如果rails初始化时,通过eager_load!加载的文件过多,显然初始化时间也越长,但是也会运行的更快,因为省去了根据class名,去读取文件的过程。

application, engines, namespace的eager_load!

在rails中,application, 各engine, 或者是namespace,如果需要在初始化时,就去加载和自己相关的文件,也就是执行eager_load!, 需要将自己的namespace注册到eager_load_namespaces中,且必须config.eager_load是true。application在初始化时,会去运行各种Railtie#initializer ,在railties/lib/application/finisher.rb中,

initializer :eager_load! do
  if config.eager_load
    ActiveSupport.run_load_hooks(:before_eager_load, self)
    config.eager_load_namespaces.each(&:eager_load!)
  end
end

那么怎么去修改config.eager_load_namespaces呢?请看

chuck@chuck-MacBook-Pro:~/seaify/rails(master|13) % grep eager_load_namespace * -ri                                                                                                                                                                
actionmailer/lib/action_mailer/railtie.rb:    config.eager_load_namespaces << ActionMailer
actionpack/lib/action_controller/railtie.rb:    config.eager_load_namespaces << ActionController
actionpack/lib/action_dispatch/railtie.rb:    config.eager_load_namespaces << ActionDispatch
actionview/lib/action_view/railtie.rb:    config.eager_load_namespaces << ActionView
activemodel/lib/active_model/railtie.rb:    config.eager_load_namespaces << ActiveModel
activerecord/lib/active_record/railtie.rb:    config.eager_load_namespaces << ActiveRecord
activesupport/lib/active_support/railtie.rb:    config.eager_load_namespaces << ActiveSupport
guides/source/configuring.md:* `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace.
guides/source/configuring.md:* `config.eager_load_namespaces` registers namespaces that are eager loaded when `config.eager_load` is true. All namespaces in the list must respond to the `eager_load!` method.
guides/source/configuring.md:* `eager_load!` If `config.eager_load` is true, runs the `config.before_eager_load` hooks and then calls `eager_load!` which will load all `config.eager_load_namespaces`.
railties/lib/rails/application/finisher.rb:          config.eager_load_namespaces.each(&:eager_load!)
railties/lib/rails/engine.rb:          Rails::Railtie::Configuration.eager_load_namespaces << base
railties/lib/rails/railtie/configuration.rb:      # Expose the eager_load_namespaces at "module" level for convenience.
railties/lib/rails/railtie/configuration.rb:      def self.eager_load_namespaces #:nodoc:
railties/lib/rails/railtie/configuration.rb:        @@eager_load_namespaces ||= []
railties/lib/rails/railtie/configuration.rb:      def eager_load_namespaces
railties/lib/rails/railtie/configuration.rb:        @@eager_load_namespaces ||= []
railties/test/application/configuration_test.rb:      assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application

可以注意到ActiveSupport, ActionDispatch, ActiveModel, ActionController等这些module, 都已经将自己的namespace追加到了config.eager_load_namespaces, 而且我们能注意到eager_load_namespaces实际上返回的是一个类变量@@eager_load_namespaces,所以实际上只有一份,各个module,gem修改的是同一个变量。

以我的实际项目homepage为例, 可以看到rails下常用的几个module的namespace都有注册到eager_load_namespace, 而那些第三方的engine,也都有注册

2.1.5 :001 > Homepage::Application.config.eager_load_namespaces
 => [ActiveSupport, ActionDispatch, ActiveModel, ActionView, ActionController, ActiveRecord, ActionMailer, Coffee::Rails::Engine, Jquery::Rails::Engine, Turbolinks::Engine, Haml::Rails::Engine, Bootstrap::Rails::Engine, RailsSettingsUi::Engine, SimpleForm, MdEmoji::Engine, Devise::Engine, SocialShareButton::Rails::Engine, Owlcarousel::Rails::Engine, Sidekiq::Rails, DisqusRails::Rails::Engine, Ahoy::Engine, Kaminari::Engine, Wice::WiceGridEngine, BootstrapDatepickerRails::Rails::Engine, Jquery::Ui::Rails::Engine, Highcharts::Rails::Engine, LazyHighCharts::Rails::Engine, FontAwesome::Rails::Engine, Spree::Core::Engine, Spree::Api::Engine, Select2::Rails::Engine, Spree::Backend::Engine, CanonicalRails::Engine, Spree::Frontend::Engine, SpreeSample::Engine, SpreeGateway::Engine, KaminariI18n::Engine, SpreeI18n::Engine, QuietAssets::Engine, Homepage::Application, ExceptionNotification::Engine

第三方的engine注册,是通过railties/lib/engine.rb, 每个engine都有将自己的namespace写入, 注意Rails::Railtie Rails::Engine Rails::Application, 是定义好的abstract_railtie,不会写入

def inherited(base)
  unless base.abstract_railtie?
    Rails::Railtie::Configuration.eager_load_namespaces << base

综上解释了,eager_load_namespaces的修改及由来,但每个namespace下,还需要有method,eager_load!

各namespace下的eager_load!

eager_load!, 实际上分了2派,一种是通过activesupport/lib/active_support/dependencies/autoload.rb提供的eager_load!, 也就是去extend ActiveSupport::Autoload, 如对于ActionMailer, ActionDispatch等

module ActionDispatch
  extend ActiveSupport::Autoload

  class IllegalStateError < StandardError
  end

  eager_autoload do
    autoload_under 'http' do
      autoload :Request
      autoload :Response
    end
  end

而activesupport/lib/active_support/dependencies/autoload.rb的实现方法

def eager_load!
  @_autoloads.each_value { |file| require file }
end

@_autoloads的由来,是因为在eager_autoload的代码块中,@_eager_autoload已经被暂时置为true, 所以其中的autoload函数,能够为@_autoloads写入路径的映射关系

def autoload(const_name, path = @_at_path)
  unless path
    full = [name, @_under_path, const_name.to_s].compact.join("::")
    path = Inflector.underscore(full)
  end

  if @_eager_autoload
    @_autoloads[const_name] = path
  end
  super const_name, path
end

上述这种办法,是针对于非engine的其它namespace.

下面介绍基于engine的eager_load!的实现

2.1.5 :005 > Homepage::Application.config.eager_load_namespaces[-2]
 => Homepage::Application
2.1.5 :006 > Homepage::Application.config.eager_load_namespaces[-2].method(:eager_load!).source_location
 => ["/Users/chuck/.rvm/gems/ruby-2.1.5/gems/railties-4.2.2/lib/rails/engine.rb", 346]
# Eager load the application by loading all ruby
# files inside eager_load paths.
def eager_load!
  config.eager_load_paths.each do |load_path|
    matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
    Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
      require_dependency file.sub(matcher, '\1')
    end
  end
end

所以注意到,在engine中,是需要去设置变量eager_load_paths, 而railties/lib/rails/engine/configuration.rb中

def eager_load_paths
  @eager_load_paths ||= paths.eager_load
end

而paths.eager_load, 是已经被设置好的,也就是app/assets, app/helpers, app/xxx/concerns, 也就是每个engine的下列eager_load是true的路径都会被加载,

def paths
  @paths ||= begin
    paths = Rails::Paths::Root.new(@root)

    paths.add "app",                 eager_load: true, glob: "{*,*/concerns}"
    paths.add "app/assets",          glob: "*"
    paths.add "app/controllers",     eager_load: true
    paths.add "app/helpers",         eager_load: true
    paths.add "app/models",          eager_load: true
    paths.add "app/mailers",         eager_load: true
    paths.add "app/views"

    paths.add "lib",                 load_path: true
    paths.add "lib/assets",          glob: "*"
    paths.add "lib/tasks",           glob: "**/*.rake"

    paths.add "config"
    paths.add "config/environments", glob: "#{Rails.env}.rb"
    paths.add "config/initializers", glob: "**/*.rb"
    paths.add "config/locales",      glob: "*.{rb,yml}"
    paths.add "config/routes.rb"

    paths.add "db"
    paths.add "db/migrate"
    paths.add "db/seeds.rb"

    paths.add "vendor",              load_path: true
    paths.add "vendor/assets",       glob: "*"

    paths
  end
end

从这些内部代码中,也可得出,若想将我们的代码,也在启动时,自动启动,则在config/application.rb中加入

config.paths.add "extras", eager_load: true
共收到 0 条回复
huacnlee 将本帖设为了精华贴 07月11日 15:41
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册