Rails Rails 源码分析之 eager_load! 篇

seaify · August 10, 2015 · Last by return replied at April 13, 2018 · 6604 hits
Topic has been selected as the excellent topic by the admin.

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
huacnlee mark as excellent topic. 11 Jul 15:41
You need to Sign in before reply, if you don't have an account, please Sign up first.