滥用link_to会造成ror程序性能下降,其中原因是什么?一个简单的link_to背后ROR到底都作了些什么?不如追随着ror的代码让我们去看个究竟。
我们通常通过如下形式调用link_to方法 <%= link_to "action_name", {:controller=>"some_controller",:action=>"some_action",:id=>xx} %>
Link_to 代码
- def link_to(name, options = {}, html_options = nil)
- url = options.is_a?(String) ? options : self.url_for(options)
- if html_options
- html_options = html_options.stringify_keys
- href = html_options['href']
- convert_options_to_javascript!(html_options, url)
- tag_options = tag_options(html_options)
- else
- tag_options = nil
- end
- href_attr = "href=\"#{url}\"" unless href
- "
- end
可以看到,link_to内部是通过url_for这个helper方法来转换hash为url路径的,让我们去看看url_for的代码
ruby 代码
- 65 def url_for(options = {})
- 66 case options
- 67 when Hash
- 68 options = { :only_path => true }.update(options.symbolize_keys)
- 69 escape = options.key?(:escape) ? options.delete(:escape) : true
- 70 url = @controller.send(:url_for, options)
- 71 when String
- 72 escape = true
- 73 url = options
- 74 when NilClass
- 75 url = @controller.send(:url_for, nil)
- 76 else
- 77 escape = false
- 78 url = polymorphic_path(options)
- 79 end
- 80
- 81 escape ? escape_once(url) : url
- 82 end
我们只关注options为hash的情况,68行将:only_path参数加入options,然后从options中去掉:escape参数,然后调用ActionController中同名的url_for方法,再调出ActionController的url_for方法
ruby 代码
- 592 def url_for(options = nil)
- 593 case options || {}
- 594 when String
- 595 options
- 596 when Hash
- 597 @url.rewrite(rewrite_options(options))
- 598 else
- 599 polymorphic_url(options)
- 600 end
- 601 end
在这个方法中,再次调用了@url的rewrite方法,rewrite_options只是空走了一遭,@url定义在方法initialize_current_url中
- 1082 def initialize_current_url
- 1083 @url = UrlRewriter.new(request, params.clone)
- 1084 end
跳啊跳,再跳到UrlRewriter中,位于actionpack/lib/action_controller/url_rewriter.rb,rewrite内部调用了UrlRewrite的私有方法rewrite_url,此时我们options中因该包含:controller,:action,:id以及后来加入的:only_path=>true
ruby 代码
- 92 def rewrite_url(options)
- 93 rewritten_url = ""
- 94
- 95 unless options[:only_path]
- 96 rewritten_url << (options[:protocol] || @request.protocol)
- 97 rewritten_url << "://" unless rewritten_url.match("://")
- 98 rewritten_url << rewrite_authentication(options)
- 99 rewritten_url << (options[:host] || @request.host_with_port)
- 100 rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
- 101 end
- 102
- 103 path = rewrite_path(options)
- 104 rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root]
- 105 rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
- 106 rewritten_url << "##{options[:anchor]}" if options[:anchor]
- 107
- 108 rewritten_url
- 109 end
调用了rewrite_path来把hash转换成url
ruby 代码
- 112 def rewrite_path(options)
- 113 options = options.symbolize_keys
- 114 options.update(options[:params].symbolize_keys) if options[:params]
- 115
- 116 if (overwrite = options.delete(:overwrite_params))
- 117 options.update(@parameters.symbolize_keys)
- 118 options.update(overwrite.symbolize_keys)
- 119 end
- 120
- 121 RESERVED_OPTIONS.each { |k| options.delete(k) }
- 122
- 123
- 124 Routing::Routes.generate(options, @request.symbolized_path_parameters)
- 125 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 代码
- 448
- 123
- 124 Routing::Routes.generate(options, @request.symbolized_path_parameters)
- 125 end
-
- 449 def generate(options, hash, expire_on = {})
- 450 write_generation
- 451 generate options, hash, expire_on
- 452 end
这个generate是一个递归调用?先不管,看看write_generation
ruby 代码
- def write_generation
-
- body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
-
-
- body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
- args = "options, hash, expire_on = {}"
-
-
- raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
-
-
-
-
-
-
- method_decl = "def generate(#{args})\npath, hash = generate_raw(options, hash, expire_on)\nappend_query_string(path, hash, extra_keys(options))\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
-
- method_decl = "def generate_extras(#{args})\npath, hash = generate_raw(options, hash, expire_on)\n[path, extra_keys(options)]\nend"
- instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
- raw_method
- end
首先动态的生成一个语句块,分别调用了generation_extraction和generation_structure,第六行根据传入参数决定是否需要给刚才定义的语句块加上条件。generation_extraction的代码如下:
ruby 代码
-
-
- def generation_extraction
- segments.collect do |segment|
- segment.extraction_code
- end.compact * "\n"
- end
segments是一个数组,其中可能存放不同种类的segment,例如segement,dynamic segment, static segment等。对segments中的元素依次调用各自的extraction_code方法,并将每次调用结果收集起来结果包装成一个数组。segments是一个数组,对其中每一个元素调用extraction_code方法,在segment类中定义了这个方法,不过方法体为空
ruby 代码
- def extraction_code
- nil
- 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
- 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 代码
- def append_query_string(path, hash, query_keys=nil)
- return nil unless path
- query_keys ||= extra_keys(hash)
- "#{path}#{build_query_string(hash, query_keys)}"
- 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 代码
- def significant_keys
- @significant_keys ||= returning [] do |sk|
- segments.each { |segment| sk << segment.key if segment.respond_to? :key }
- sk.concat requirements.keys
- sk.uniq!
- end
- end
to_query方法定义在activesupport/lib/active_support/core_ext/hash/conversions.rb 中