现在,我们已经大致了解了link_to
方法的内部,但有几个细节需要继续探索:
link_to
内部调用的方法convert_options_to_data_attributes
到底在做什么?- 为什么
link_to
方法中要freeze
一些字符串? contant_tag
方法最终是如何生成<a>
标签的?
整理 html 属性
首先,从convert_options_to_data_attributes
的命名可以看出,这个方法是为了把options中的一些相关字段转化为页面元素的属性,而且从下面的详细代码中可以看出,这个 options 包括 options 和 html_options。另外,这个方法在button_to
等方法中也有调用,目的都是整理页面元素的属性。那么,Rails 想用这个方法来整理哪些属性呢?
在这个方法内部,
1 | def convert_options_to_data_attributes(options, html_options) |
我们可以看到,因为从link_to
内部传入的 options
和 html_options
参数都可能是 nil,所以会有很多不同情况的判断条件。
这个方法处理的第一个属性是data-remote
,而这个属性的设置方法可以是link_to 'a link', products_path, method: :post, remote: true
,经过几行代码的处理之后,remote
字段会被以{"data-remote" => "true"}
的形式表示在 html_options 这个变量中。
另外,我们可以看到,Rails 其实考虑了remote
字段不在 html_options 中,而在 options 参数中传入的情况。也就是说,如果你写出这样的代码,Rails 能巧合的生成正确的结果:
1 | link_to 'a strange syntax', controller: 'products', action: 'index', remote: true |
他们都会生成类似下面的元素:
<a data-remote="true" href="/products">a ... syntax</a>
这在你使用 ajax 去 GET 远端的资源时或许用的上,3种语法任意选,不过正常来看,选择把remote: true
放在 html_options 中更好,也就是上面的最后一种语法。
整理了 remote
字段后,Rails 会整理 method
字段。不过method
就不像remote
这么特殊了,它只能被写在 html_options 这个参数中。而在整理 method 字段时,Rails 又用到一个封装的方法add_method_to_attributes!
:
1 | def add_method_to_attributes!(html_options, method) |
这个方法做的事情就是根据一定规则创建rel
字段(如果不是get
方法,如果rel
字段没有被明确地写为带nofollow
的 link type,则修改 rel
字段)和data-method
字段。
到此为止,html_options 中剩下的字段只可能是data-remote
,data-method
,rel
以及class
等其他常用或自定义的属性。
而 options 中一定不会存在 remote
字段了。
为什么freeze
到这里,我们其实可以解释一下,为什么要 freeze
。
freeze
其实是两年前(i.e. 2016 年)才被 merge 进 Rails 的源代码的,而且整个 url_helper 模块的freeze
都是一个人的贡献,他 commit 时留下的解释是:
reduce string allocation
因为 url_helper 这个方法经常被用到,所以如果减少这里面使用字符串时的内存重新分配,可以提高性能。
其实,当人们在调用freeze
的时候,通常想让他做的事可能是:
- 防止对象被更改
- 检查内存的重新分配
第一种情况通常发生在人们把一个对象放在变量中时,比如a = {key: 'value'}
。如果a
不可修改,但别人有很难从变量的命名或者代码的其他地方看出a
不可修改,那么这个时候可以通过a = {key: 'value'}.freeze
把这个 Hash 对象锁住,让所有尝试改动此对象的人得到一个异常。
第二种情况才是 url_helper 这个 module 中要使用freeze
的原因。url_helper 中都没有把那些 String 对象赋值给一个变量,谈不上防止别人在别处修改变量中的对象。而使用 freeze 真的能减少内存分配么?这是真的,我们可以用object_id
方法做个检验:
1 | def no_allocation |
object_id 方法是对象的唯一标示,如果object_id 不同,则代表是不同的对象,存储位置也不一样。但是上面的实验显示两次使用'a string'.freeze
得到的是同一个对象,Ruby 没有重新分配一块内存来存储新的 String 对象。
但如果不用freeze
的话,情况就不一样了:
1 | def yes_allocation |
每次调用yes_allocation
方法,内存都会新分配一个对方存储新的'a string'
。
生成<a>
现在的局面是跟 url 有关的字段都在 options 这个变量中,而跟元素其他属性相关的字段都以字典的形式存在 html_options 中,除了href
属性。
Rails 会用url_for(options)
生成href
属性需要的字符串,然后也存放到html_options
中,就是这两行代码:
1 | url = url_for(options) |
至于url_for
方法是怎么生成 url 的,我们以后再讨论。
更重要的是当所有这些<a>
的元素都整理好之后,Rails 会用content_tag
方法来生成<a>
,怎么做的呢:
1 | content_tag("a".freeze, name || url, html_options, &block) |
而在 content_tag
方法中又发生了什么呢?这可以是独立的一节来描述,所以我们放到下一节。而且,可以想见,如果了解了 content_tag
方法,其他类似的 tag 方法都可以类似的理解,比如image_tag
。
Recap
到这里,我们可以小小的总结一下,我们从link_to
这个在 ActionView 模块中定义的方法中看到了什么:
- 利用含有默认值的 positional parameters 和接受 code block 类型的参数,使得调用方法的语法特别多样。
- 在 block_given? 的情况下轮换参数顺序,使得在外部调用时感觉不到传参上的不便,比如出现
link_to(nil, 'http://example.com') { '<span>Example</span>' }
这样必须传一个 nil 来站位的情况。 - 封装方法,处理有很多个字段的字典,而不是在方法内部直接处理。在
link_to
的外部,只需要使用remote
等很简单的字段。转换成 html 元素属性的工作交给内部封装的方法。 - 对于经常调用的方法中需要不断使用的同一字符串进行 freeze,减少 ruby 重新分别内存的次数,提高性能。这样的字符串也不适合复制给常量,因为使用的时候不够直接,所以就地
freeze
。