根据rails guide里constant reloading一节的描述,如果修改了某些代码,想在rails console里重新加载,可输入reload!

此命令其实是调用railties-5.2.1/lib/rails/console/app.rb中的Rails.application.reloader.reload!

于是跟踪一下:

[3] pry(main)> binding.trace_tree(htmp: 'rails_reload_bang'){ Rails.application.reloader.reload! }

得调用栈如下

20190610_141305_091_rails_reload_bang.html

从activesupport-5.2.1/lib/active_support/reloader.rb来看,这reloader的实现比较复杂,它在实际执行run!和complete!的前后,还要执行ExecutionWrapper的run!和complete!,而这些run!和complete!其实是callback……所以调用栈来回跳跃,还包含了一大堆callback机制的调用……

# activesupport-5.2.1/lib/active_support/reloader.rb
module ActiveSupport
  class Reloader < ExecutionWrapper

    def self.reload!
      executor.wrap do
        new.tap do |instance|
          begin
            instance.run!
          ensure
            instance.complete!
          end
        end
      end
      prepare!
    end

# activesupport-5.2.1/lib/active_support/execution_wrapper.rb
module ActiveSupport
  class ExecutionWrapper
    include ActiveSupport::Callbacks

    def self.wrap
      return yield if active?
      instance = run!
      begin
        yield
      ensure
        instance.complete!
      end
    end

甚至,reloader里根本没包含任何reload的逻辑,只能从该文件里的一些特别的字眼,如class_unload,去dump出的调用栈里搜索,结果还是发现一些蛛丝马迹:


如上,Rails.application.reloader.reload!是会调用class_unload!的,而这个方法会转而调用railties-5.2.1/lib/rails/application/finisher.rb里定义的回调,该回调执行的就是ActiveSupport::DescendantsTracker.clear和ActiveSupport::Dependencies.clear

# railties-5.2.1/lib/rails/application/finisher.rb
module Rails
  class Application
    module Finisher
      include Initializable

      initializer :set_clear_dependencies_hook, group: :all do |app|
        callback = lambda do
          ActiveSupport::DescendantsTracker.clear
          ActiveSupport::Dependencies.clear
        end

        if config.cache_classes
          app.reloader.check = lambda { false }
        elsif config.reload_classes_only_on_change
          app.reloader.check = lambda do
            app.reloaders.map(&:updated?).any?
          end
        else
          app.reloader.check = lambda { true }
        end

        if config.reload_classes_only_on_change
          reloader = config.file_watcher.new(*watchable_args, &callback)
          reloaders << reloader

          # Prepend this callback to have autoloaded constants cleared before
          # any other possible reloading, in case they need to autoload fresh
          # constants.
          app.reloader.to_run(prepend: true) do
            # In addition to changes detected by the file watcher, if routes
            # or i18n have been updated we also need to clear constants,
            # that's why we run #execute rather than #execute_if_updated, this
            # callback has to clear autoloaded constants after any update.
            class_unload! do
              reloader.execute
            end
          end
        else
          app.reloader.to_complete do
            class_unload!(&callback)
          end
        end
      end

其中ActiveSupport::DescendantsTracker.clear用于对include了ActiveSupport::DescendantsTracker的类清理其对子类的跟踪信息

而ActiveSupport::Dependencies.clear代码量就有点大了,于是另外再跟踪一次:

[7] pry(main)> binding.trace_tree(htmp: 'dep_clear'){ ActiveSupport::Dependencies.clear }

得调用栈如下

20190611_145854_583_dep_clear.html

其实基本骨架还是比较清晰的

def clear
  Dependencies.unload_interlock do
    loaded.clear
    loading.clear
    remove_unloadable_constants!
  end
end

运行起来就是这样


这里面loaded和loading保存的都是文件路径,用于记录哪些代码文件已经加载过,而remove_unloadable_constants!则是调用remove_constant清除常量,其实现如下

def remove_constant(const) #:nodoc:
  # Normalize ::Foo, ::Object::Foo, Object::Foo, Object::Object::Foo, etc. as Foo.
  normalized = const.to_s.sub(/\A::/, "")
  normalized.sub!(/\A(Object::)+/, "")

  constants = normalized.split("::")
  to_remove = constants.pop

  # Remove the file path from the loaded list.
  file_path = search_for_file(const.underscore)
  if file_path
    expanded = File.expand_path(file_path)
    expanded.sub!(/\.rb\z/, "")
    loaded.delete(expanded)
  end

  if constants.empty?
    parent = Object
  else
    # This method is robust to non-reachable constants.
    #
    # Non-reachable constants may be passed if some of the parents were
    # autoloaded and already removed. It is easier to do a sanity check
    # here than require the caller to be clever. We check the parent
    # rather than the very const argument because we do not want to
    # trigger Kernel#autoloads, see the comment below.
    parent_name = constants.join("::")
    return unless qualified_const_defined?(parent_name)
    parent = constantize(parent_name)
  end

  # In an autoloaded user.rb like this
  #
  #   autoload :Foo, 'foo'
  #
  #   class User < ActiveRecord::Base
  #   end
  #
  # we correctly register "Foo" as being autoloaded. But if the app does
  # not use the "Foo" constant we need to be careful not to trigger
  # loading "foo.rb" ourselves. While #const_defined? and #const_get? do
  # require the file, #autoload? and #remove_const don't.
  #
  # We are going to remove the constant nonetheless ---which exists as
  # far as Ruby is concerned--- because if the user removes the macro
  # call from a class or module that were not autoloaded, as in the
  # example above with Object, accessing to that constant must err.
  unless parent.autoload?(to_remove)
    begin
      constantized = parent.const_get(to_remove, false)
    rescue NameError
      # The constant is no longer reachable, just skip it.
      return
    else
      constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
    end
  end

  begin
    parent.instance_eval { remove_const to_remove }
  rescue NameError
    # The constant is no longer reachable, just skip it.
  end
end

实际上它就是找出一个常量的父级命名空间,然后在其中调用remove_const,并顺便调用before_remove_const(如果有定义的话)

至此,class和module什么的应该都被清掉了,当再次读到常量名字时,就又会走Dependencies里定义的const_missing回调去加载代码,实现reload