Rails ActiveRecord 中的 Callback 浅析

loveltyoic · November 21, 2014 · Last by JefferyXHY replied at December 21, 2018 · 6681 hits

在使用 Rails 或者说 ActiveRecord 的过程中,我们都会用一些 callback 来使我们的代码更紧凑简洁。 我们只需要知道用法就可以了,但是你是否也会好奇,这些神奇的 callback 是怎么实现的呢? 下面通过源码加注释的方式粗略的说明一下我的探究过程。

首先定义 User 类,并给他添加几个 callback,用了 3 种形式

class ShowMyself
  def before_validation(model)
    puts 'def callback by object'
  end
end

class User < ActiveRecord::Base
  before_create :say_hello
  def say_hello
    puts 'def callback by method'
  end

  before_validation ShowMyself.new

  before_save do 
    puts 'def callback by block'
  end

end

那么问题来了,before_validation ShowMyself.new是怎么工作的呢?

  • before_validation开始查找
def before_validation(*args, &block)
  options = args.last
  if options.is_a?(Hash) && options[:on]
    options[:if] = Array(options[:if])
    options[:on] = Array(options[:on])
    options[:if].unshift lambda { |o|
      options[:on].include? o.validation_context
    }
  end
  set_callback(:validation, :before, *args, &block)
  # set_callback(:validation, :before, ShowMyself.new)
end
  • 进入set_callback(:validation, :before, ShowMyself.new)
def set_callback(name, *filter_list, &block)

  # name = :validation
  # filter_list = [:before, ShowMyself.new]

  type, filters, options = normalize_callback_params(filter_list, block)

  # 调用normalize_callback_params,得到如下返回

  # type = :before
  # filters = [ShowMyself.new]
  # options = {}

  self_chain = get_callbacks name

  # get_callbacks 得到 CallbackChain类型的实例, 
  # 在console里面通过User._validation_callbacks就可以得到它了
  # 其他的如_save_callbacks, _create_callbacks都一样可以看到

  mapped = filters.map do |filter|
    Callback.build(self_chain, filter, type, options)
    # Callback.build(self_chain, ShowMyself.new, :before, {})
    # 这里可以暂时知道它是一个Callback实例就可以了,后面会有说明
  end
  # mapped = [以ShowMyself.new为filter的Callback实例]

  # 记住name = :validation
  __update_callbacks(name) do |target, chain|
    # 在当前block中
    # target = User
    # chain = User.get_callbacks :validation

    options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
    # 把mapped加入到回调链中

    target.set_callbacks name, chain
    # 即User._validation_callbacks= chain
    # 执行到这里,ShowMyself.new就添加成功了
    # 可以执行User._validation_callbacks.first.filter看看
    # 应该看到类似如下结果
    # <ShowMyself:0x007ff556309938>
    # 正是ShowMyself的实例
  end
end
# File activesupport/lib/active_support/callbacks.rb, line 740
def set_callbacks(name, callbacks)
  send "_#{name}_callbacks=", callbacks
end
  • normalize_callback_params ```ruby
    def normalize_callback_params(filters, block) # :nodoc: type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before # type = filters 中 (即传过来的 [:before, ShowMyself.new]) 第一个元素 = :before

options = filters.extract_options! filters.unshift(block) if block [type, filters, options.dup] end

- `__update_callbacks`
```ruby
def __update_callbacks(name) #:nodoc:
  ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
    #当前类User以及他的子类都需要新增这个callback, 即回调也是会继承的
    chain = target.get_callbacks name
    yield target, chain.dup
  end
end

以上是定义 callback 的过程,那么 callback 是怎么执行的呢?

当执行 user.valid?的时候,其实调用了run_validations!

def run_validations! #:nodoc:
  _run_validation_callbacks { super }
end

then

def _run_#{name}_callbacks(&block)
  _run_callbacks(_#{name}_callbacks, &block)
  # _run_callbacks(_validation_callbacks, &block)
end

then

def _run_callbacks(callbacks, &block)
  # 这里的callbacks就是User._validation_callbacks
  if callbacks.empty?
    block.call if block
  else
    runner = callbacks.compile
    # compile 是CallbackChain中定义的实例方法,返回一个lambda

    # runner就是如下形式的lambda
    # lambda { |env|
    #   user_callback.call env.target, env.value
    #   next_callback.call env
    # }

    # user_callback也是一个lambda
    # lambda { |target, _, &blk|
    #    filter.public_send method_to_call, target, &blk
    # }

    # 当前的self就是User实例,即user
    # Environment从后面可以知道就是一个Struct
    e = Filters::Environment.new(self, false, nil, block)
    runner.call(e).value
  end
end
  • CallbackChain 的定义 (节选)
class CallbackChain #:nodoc:#
  # 一个可遍历的链
  include Enumerable

  attr_reader :name, :config

  def compile
    @callbacks || @mutex.synchronize do
      @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback|
        callback.apply chain
        #进入Callback::apply
        #此时
        #callback.name = :validation
        #callback.kind = :before
      end
    end
  end

end
class Callback #:nodoc:#
  def self.build(chain, filter, kind, options)
    new chain.name, filter, kind, options, chain.config
    # 此时
    # chain.name = :validation
    # filter = ShowMyself.new
  end

  attr_accessor :kind, :name
  attr_reader :chain_config

  def initialize(name, filter, kind, options, chain_config)
    @chain_config  = chain_config
    @name    = name
    @kind    = kind
    @filter  = filter
    @key     = compute_identifier filter
    @if      = Array(options[:if])
    @unless  = Array(options[:unless])
  end

  def apply(next_callback)

    user_conditions = conditions_lambdas
    user_callback = make_lambda @filter
    # 当前的@filter 就是ShowMyself.new
    # make_lambda很关键!定义在下面

    case kind
    when :before
      Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter)
    when :after
      Filters::After.build(next_callback, user_callback, user_conditions, chain_config)
    when :around
      Filters::Around.build(next_callback, user_callback, user_conditions, chain_config)
    end
  end

  def make_lambda(filter)
    case filter
    # 当filter是一个方法的时候, 如 :say_hello, 执行user.send :say_hello
    when Symbol
      lambda { |target, _, &blk| target.send filter, &blk }
    # 可以用string来定义写filter,用eval执行
    when String
      l = eval "lambda { |value| #{filter} }"
      lambda { |target, value| target.instance_exec(value, &l) }
    when Conditionals::Value then filter
    when ::Proc
      if filter.arity > 1
        return lambda { |target, _, &block|
          raise ArgumentError unless block
          target.instance_exec(target, block, &filter)
        }
      end

      if filter.arity <= 0
        lambda { |target, _| target.instance_exec(&filter) }
      else
        lambda { |target, _| target.instance_exec(target, &filter) }
      end
    else
      # ShowMyself.new的情况
      # scope=>[:kind, :name]
      scopes = Array(chain_config[:scope])
      method_to_call = scopes.map{ |s| public_send(s) }.join("_")
      # method_to_call = "before_validation"

      lambda { |target, _, &blk|
        filter.public_send method_to_call, target, &blk
        # ShowMyself.new.public_send "before_validation", user
        # 看这个lambda, 再回头看看ShowMyself里
        # def before_validation(model)
        #   p model.inspect
        #   puts 'def callback by class'
        # end
        # 转了一圈,终于真相大白了
      }
    end
  end

end

附录

callback 的主要实现都在 ActiveSupport::Callbacks 中

# activrecord/lib/active_record/callbacks.rb

module ActiveRecord
  module Base
    include Callbacks
  end

  module Callbacks
    extend ActiveSupport::Concern
    module ClassMethods
      include ActiveModel::Callbacks
    end
  end
  # 上面翻译过来就是 ActiveRecord::Base.extend ActiveModel::Callbacks
end
# activemodel/lib/active_model/callbacks.rb

moudle ActiveModel
  module Callbacks
    def self.extended(base) #:nodoc:
      base.class_eval do
        include ActiveSupport::Callbacks
        # 结果就是 ActiveRecord::Base.include ActiveSupport::Callbacks
      end
    end
  end
end

我感觉自己像《天龙八部》里面的梅竹兰菊四姐妹一样,没有深厚的内力来学逍遥派的上乘武功,迟早会走火入魔。我还是先把基础打好才能不误入歧途吧。

看了将近两个小时,还是有些懵懂,最后

lambda { |target, _, &blk|

这里似乎有些开朗了,表面上看起来很简单的东西,后面需要很强大很复杂的逻辑来支撑。很敬佩楼主专研的精神。我也很惭愧,我不是一个好程序员。

一直对回调这一点有疑问

before_save { self.email = email.downcase }

对于这段代码,这里的 self 为什么会是实例而不是这个类本身?

#4 楼 @vary_ before_save 是实例方法还是类方法?

Reply to vary_

block 的 代码实际是在 instance 执行 save 时才会调用的,你可以理解是 before_save 只是往 isntance 的 save 方法中插入了{ self.email = email.downcase} 这句代码

You need to Sign in before reply, if you don't have an account, please Sign up first.