Callbacks 底层简要分析

ActiveSupport::Callbacks 本身可分为几部分。

Callback Chain 回调链

define_callbacks 主要作用就是定义一条"回调链",每一条链都是 Callback Chain 的实例对象。

Callback Chain 实例对象,大致如下:

#<ActiveSupport::Callbacks::CallbackChain:0x007fc6ebf70648
  @callbacks=nil, 
  @chain=[], 
  @config={:scope=>[:kind]}, 
  @mutex=#<Mutex:0x007fc6ebf70580>, 
  @name=:checkout>

其中,最重要的信息有:

  • @chain 此回调链所包含的"所有回调"。

  • @name 此回调链的"名字"。

Rails 没有提供查看所有 callback chain 信息的接口,只能间接查看。

比如:

# 所有"回调链"
callback_chains = ObjectSpace.each_object(ActiveSupport::Callbacks::CallbackChain)
# 不为空的"回调链"
chains = callback_chains.select{|cc| cc.instance_variable_get("@chain").present? }

# 所有"回调链"的名字
callback_chains_uniq_name = chains.map(&:name).uniq

但对于某个类而言,则很方便。如:

callback_chains = ClassName.singleton_methods.select do |method|
  method.to_s =~ /^_[^_].+_callbacks$/
end

# Rails model 默认有 callback chains:
:_save_callbacks
:_create_callbacks
:_update_callbacks
:_validate_callbacks
:_validation_callbacks
:_initialize_callbacks
:_find_callbacks
:_touch_callbacks
:_destroy_callbacks
:_commit_callbacks
:_rollback_callbacks

# 查看某 callback chain 详情。如:
User._save_callbacks

Callback 回调

上面提到每一条回调链的 @chain 里包含了它"所有的回调",这里的每一个"回调"就是一个 Callback 实例对象。

Callback 实例对象,大致如下:

#<ActiveSupport::Callbacks::Callback:0x007fc6f2162248
  @chain_config={:scope=>[:kind]},
  @filter=#<Proc:0x007fc6f2162388@/Users/.../lib/rails/application/finisher.rb:100>,
  @if=[],
  @key=70246220894660,
  @kind=:before,
  @name=:prepare,
  @unless=[]

其中,最重要的信息有:

  • @if@unless 此回调起作用的"前提条件"。

  • @kind 此回调的"回调类型"。(对于 Rails 而言,可以选择 :before, :after, :around 其中之一)

  • @name 此回调所加入的"回调链的名字"。

  • @key 此回调的"名字"。(如果没有名字则用 object_id 代替)

  • @filter 此回调"真正要执行的代码"。(一般只显示名字,也就是说和 @key 一样;如果没有名字,则显示定义它时的文件及行号)

"真正要执行的代码",可以是以下类型:

Symbols:: 方法名。
Strings:: 可求值的字符串。
Procs::   proc 对象。
Objects:: 普通的实例对象,但必需有 before_x, around_x, after_x 等"回调"方法。
          因为,之后会以拼接字符串的形式,找出对应的回调方法,然后 send 调用。

其实,还有一种类型 Conditionals 但被直接跳过了,所以不算在内。

这些不同类型的"真正要执行的代码",之后都会被 Callback 的 make_lambda 方法转换成 lambda 对象,再然后处理过程类似。

其它

当 @filter 是一个实例对象,并且恰好有和"回调类型"相同的方法

上面也提到"回调类型",Rails 默认有 :before、:after 和 :around. 上面提到了回调里"真正要执行的代码"可以是一个实例对象,当触发回调时,会执行它的同名"回调"方法。

如果 @filter 恰好是一个实例对象,而这个实例对象又恰好有 :before、:after 或 :around,则此时会出现和想像中不一样的结果。

比如:

require 'active_support'

class Audit
  def before(caller)
    puts 'Audit: before is called'
  end

  def before_save(caller)
    puts 'Audit: before_save is called'
  end
end

class Account
  include ActiveSupport::Callbacks

  define_callbacks :save
  set_callback :save, :before, Audit.new

  def save
    run_callbacks :save do
      puts 'save in main'
    end
  end
end

Account.new.save

此时,我们可以使用 define_callbacks 的 scope 参数进行解决。也就是:

define_callbacks :save, scope: [:kind, :name]

Rails 里 Callback 相关的模块及继承关系

ActiveRecord::Callbacks
           |
           V
ActiveModel::Callbacks
           |
           V
ActiveSupport::Callbacks
ActiveModel::Validations::Callbacks
ActiveJob::Callbacks
ActionDispatch::Callbacks
AbstractController::Callbacks
         |
         V
ActiveSupport::Callbacks

Filters 顺序

一条"回调链"上可以有多个"回调",它们彼此之间不是独立的,有先后顺序。即:

  • Before,After,Around

但这些"回调"也有终结的时候。即:

  • End

定义、运行回调

define_callbacks 定义的时候会:

define_callbacks
       |
       V
set_callbacks name, CallbackChain.new(name, options)  # 这里的 name 表示"回调链"的名字。

def set_callbacks(name, callbacks) # 这里的 callbacks 是"回调链"的实例对象。
  send "_#{name}_callbacks=", callbacks
end

define_callbacks
       |
       V
def _run_#{name}_callbacks(&block)
  _run_callbacks(_#{name}_callbacks, &block)
end

run_callbacks 运行的时候会:

run_callbacks
      |
      V
_run_#{kind}_callbacks # 这里的 kind 表示"某种类型的"回调链
      |
      V
_run_callbacks(_#{name}_callbacks, &block) # 这里的 name 表示"回调链"
      |
      V
runner = callbacks.compile # 这里的 callbacks 表示"回调链";
                           # compile 会创建 Callback 实例对象并做后续处理。
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value

最后更新于