一个简单的link_to,ROR到底在背后做了些什么

滥用link_to会造成ror程序性能下降,其中原因是什么?一个简单的link_to背后ROR到底都作了些什么?不如追随着ror的代码让我们去看个究竟。

我们通常通过如下形式调用link_to方法 <%= link_to "action_name", {:controller=>"some_controller",:action=>"some_action",:id=>xx} %>

Link_to 代码
   1. def link_to(name, options = {}, html_options = nil)    
   2.     url = options.is_a?(String) ? options : self.url_for(options)    
   3.     if html_options    
   4.        html_options = html_options.stringify_keys    
   5.        href = html_options['href']    
   6.        convert_options_to_javascript!(html_options, url)    
   7.        tag_options = tag_options(html_options)    
   8.     else    
   9.        tag_options = nil    
  10.     end    
  11.     href_attr = "href=\"#{url}\"" unless href    
  12.     "    
  13. end    

可以看到,link_to内部是通过url_for这个helper方法来转换hash为url路径的,让我们去看看url_for的代码

ruby 代码
       def url_for(options = {})    
         case options    
         when Hash    
           options = { :only_path => true }.update(options.symbolize_keys)    
           escape  = options.key?(:escape) ? options.delete(:escape) : true    
           url     = @controller.send(:url_for, options)    
         when String    
          escape = true    
          url    = options    
         when NilClass    
           url = @controller.send(:url_for, nil)    
         else    
          escape = false    
          url    = polymorphic_path(options)    
         end    
    
       escape ? escape_once(url) : url    
    end    

我们只关注options为hash的情况,将:only_path参数加入options,然后从options中去掉:escape参数,然后调用ActionController中同名的url_for方法,再调出ActionController的url_for方法

ruby 代码
      def url_for(options = nil) #:doc:    
       case options || {}    
           when String    
             options    
           when Hash    
             @url.rewrite(rewrite_options(options))    
           else    
             polymorphic_url(options)    
         end    
         end    

在这个方法中,再次调用了@url的rewrite方法,rewrite_options只是空走了一遭,@url定义在方法initialize_current_url中

def initialize_current_url    
        @url = UrlRewriter.new(request, params.clone)    
      end 

跳啊跳,再跳到UrlRewriter中,位于actionpack/lib/action_controller/url_rewriter.rb,rewrite内部调用了UrlRewrite的私有方法rewrite_url,此时我们options中因该包含:controller,:action,:id以及后来加入的:only_path=>true

ruby 代码
      def rewrite_url(options)    
         rewritten_url = ""    
     
         unless options[:only_path]    
           rewritten_url << (options[:protocol] || @request.protocol)    
           rewritten_url << "://" unless rewritten_url.match("://")    
           rewritten_url << rewrite_authentication(options)    
rewritten_url << (options[:host] || @request.host_with_port)    
            rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)    
           end    
         path = rewrite_path(options)    
         rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]    
         rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)    
         rewritten_url << "##{options[:anchor]}" if options[:anchor]    
    
        rewritten_url    
     end  

调用了rewrite_path来把hash转换成url

ruby 代码
def rewrite_path(options)    
         options = options.symbolize_keys    
         options.update(options[:params].symbolize_keys) if options[:params]    
     
         if (overwrite = options.delete(:overwrite_params))    
          options.update(@parameters.symbolize_keys)    
           options.update(overwrite.symbolize_keys)    
         end    
     
         RESERVED_OPTIONS.each { |k| options.delete(k) }    
     
       # Generates the query string, too    
         Routing::Routes.generate(options, @request.symbolized_path_parameters)    
       end   

RESERVED_OPTIONS是一个数组,包含一些控制用的参数,从options中删除它们,仅仅留下:controller,:action,:id

ruby 代码
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]

最后,调用Routing::Routes中的generate方法将:controller,:action,:id拼接成url

ruby 代码
# # Write the real generation implementation and then resend the message.   
         # Generates the query string, too    
         Routing::Routes.generate(options, @request.symbolized_path_parameters)    
       end    
  
       def generate(options, hash, expire_on = {})    
        write_generation    
         generate options, hash, expire_on    
       end 

这个generate是一个递归调用?先不管,看看write_generation

ruby 代码
   1. def write_generation  
   2.   # Build the main body of the generation  
   3.   body = "expired = false\n#{generation_extraction}\n#{generation_structure}"  
   4.   
   5.   # If we have conditions that must be tested first, nest the body inside an if  
   6.   body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements  
   7.   args = "options, hash, expire_on = {}"  
   8.   
   9.   # Nest the body inside of a def block, and then compile it.  
  10.   raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"  
  11.   instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"  
  12.   
  13.   # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash  
  14.   # are the same as the keys that were recalled from the previous request. Thus,  
  15.   # we can use the expire_on.keys to determine which keys ought to be used to build  
  16.   # the query string. (Never use keys from the recalled request when building the  
  17.   # query string.)  
  18.   
  19.   method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"  
  20.   instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"  
  21.   
  22.   method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"  
  23.   instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"  
  24.   raw_method  
  25. end  

首先动态的生成一个语句块,分别调用了generation_extraction和generation_structure,第六行根据传入参数决定是否需要给刚才定义的语句块加上条件。generation_extraction的代码如下:

ruby 代码
 # Build several lines of code that extract values from the options hash. If any  
 # of the values are missing or rejected then a return will be executed.  
 def generation_extraction  
   segments.collect do |segment|  
     segment.extraction_code  

   1.   end.compact * "\n"  

 end

segments是一个数组,其中可能存放不同种类的segment,例如segement,dynamic segment, static segment等。对segments中的元素依次调用各自的extraction_code方法,并将每次调用结果收集起来结果包装成一个数组。segments是一个数组,对其中每一个元素调用extraction_code方法,在segment类中定义了这个方法,不过方法体为空

ruby 代码
   1. def extraction_code  
   2.   nil  
   3. end  

在segment的子类DynamicSegement中重定义了此方法

java 代码
代码
#相关代码
def extraction_code
s
= extract_value
vc
= value_check
s
<< "\nreturn [nil,nil] unless #{vc}" if vc
s
<< "\n#{expiry_statement}"
end

def extract_value
"#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
end

def value_check
if default # Then we know it won't be nil
"#{value_regexp.inspect} =~ #{local_name}" if regexp elsif optional?
# If we have a regexp check that the value is not given, or that it matches.
# If we have no regexp,
return nil since we do not require a condition.
"#{local_name}.nil? || #{value_regexp.inspect} =~ {local_name}" if regexp
else # Then it must be present, and if we have a regexp, it must match too.
"#{local_name} #{"&& #{value_regexp.inspect} =~ {local_name}" if regexp}"
end
end

def expiry_statement
"expired, hash = true, options if !expired && expire_on[:#{key}]"
end

 

 

根据上面几个方法的源代码,可以推断出write_generation中共动态生成了三个方法,列出方法如下:

ruby 代码
 def generate_raw(options, hash, expire_on = {})  
    path = begin  
      expired = false  
      key_value = hash[:key] && hash[:key].to_param (|| option[:default] )  
      return [nil, nil] unless (key_value.nil? ||) /\A#{regexp.source}\Z/.inspect =~key_value  
      expired, hash = true, options if !expired && expire_on[key]  
    end  
    [path, hash]  
 end  
   
 def generate(options, hash, expire_on={})  
   path, hash = generate_raw(options, hash, expire_on)  
   append_query_string(path, hash, extra_keys(options)  
 end  
   
 def generate_extras(options, hash, expire_on = {})  
   path, hash = generate_raw(options, hash, expire_on)  
   [path, extra_keys(options)]  
 end   

注意在值钱的generate方法中曾经调用国generate方法本身,之前我以为是递归,现在看来,是在generate方法中调用write_generate方法重新生成了一个generate方法,然后调用此方法,也就是上面生成的三个方法之一。看来已经一步步接近真相了,在新生成的方法中调用了append_query_string方法,并且接受被extra_keys方法处理过的options作为参数。

 

ruby 代码
   1. def append_query_string(path, hash, query_keys=nil)  
   2.   return nil unless path  
   3.   query_keys ||= extra_keys(hash)  
   4.   "#{path}#{build_query_string(hash, query_keys)}"  
   5. end  

 

ruby 代码
def extra_keys(hash, recall={})  
   (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys  
 end  
   
 def build_query_string(hash, only_keys = nil)  
   elements = []  
   
   (only_keys || hash.keys).each do |key|  
     if value = hash[key]  
       elements << value.to_query(key)  
     end  
   end  
   
   elements.empty? ? '' : "?#{elements.sort * '&'}"  
 end 

 

ruby 代码
   1. def significant_keys  
   2.   @significant_keys ||= returning [] do |sk|  
   3.     segments.each { |segment| sk << segment.key if segment.respond_to? :key }  
   4.     sk.concat requirements.keys  
   5.     sk.uniq!  
   6.   end  
   7. end  

to_query方法定义在activesupport/lib/active_support/core_ext/hash/conversions.rb 中

 

备注:本文章转自http://9esuluciano.javaeye.com/blog/123913

posted @ 2010-11-25 11:36  lonelystarxing  阅读(539)  评论(0编辑  收藏  举报