Skip to content

Instantly share code, notes, and snippets.

@ambar
Created April 25, 2012 15:44
Show Gist options
  • Save ambar/2490775 to your computer and use it in GitHub Desktop.
Save ambar/2490775 to your computer and use it in GitHub Desktop.
Closer Library 特定版本编译可能会产生不能意料的 BUG

goog.events.listen bug

来源

原始代码:

// closure_library_path/closure/goog/events/events.js L222
// goog.events.listen = function(src, type, listener, opt_capt, opt_handler) {....
// Attach the proxy through the browser's API
if (src.addEventListener) {
  if (src == goog.global || !src.customEvent_) {
    src.addEventListener(type, proxy, capture);
  }
} else {
  src.attachEvent(goog.events.getOnString_(type), proxy);
}

Closure 在会检测要绑定事件的目标对象上的 customEvent_ 属性,此值为 true 时它表示目标对象不是 DOM 原生对象,而是一个 goog.events.EventTarget 实例,同时这个事件的可能是一个自定义的事件。

customEvent_ 属性来源:

// closure_library_path/closure/goog/events/eventtarget.js L72
/**
 * Used to tell if an event is a real event in goog.events.listen() so we don't
 * get listen() calling addEventListener() and vice-versa.
 * @type {boolean}
 * @private
 */
goog.events.EventTarget.prototype.customEvent_ = true;

原因

高级编译模式时,customEvent_ 被编译成了其他随机的短名称,比如有 pw、tw、Lx:

env 1:

if (a.addEventListener) {
    if (a == q || !a.pw)
        a.addEventListener(b, j, d)
}

env 2:

if (a.addEventListener) {
    if (a == q || !a.tw)
        a.addEventListener(b, j, d)
}

env 3:

if (a.addEventListener) {
    if (a == q || !a.Lx)
        a.addEventListener(b, j, d)
}

同时,如果有一个类似的表单,表单的 pw 属性为 input 元素,而 customEvent_ 也被编译成 pw 短名称,此时 goog.events.listen 绑定事件将失效,如:

<form id="myform">
    <input name="pw">
</form>
<script>
    var form = document.forms['myform']
    // input element
    console.log(form.pw)
    // failed to listen
    goog.events.listen(form, 'submit', fn)
</script>

避免

暂时想了两个办法:

  • 修改源码
if (src == goog.global || src.customEvent_ !== true) {..}
  • 或 input 使用足够长的 name 属性,不可靠。
  • 更换 closure library 版本,跳过这个编译结果,不可靠.
@ambar
Copy link
Author

ambar commented Apr 28, 2012

现在也只发现只有 form 会有绑定失效的情况。

关于 input 遍历:
form.name 访问肯定看着就很恶心,还有一种推荐的办法:form.elements['name'] ,这样可以免去元素遍历,速度更快:

比如 jQuery.serializeArray 方法:
https://github.com/jquery/jquery/blob/master/src/ajax.js#L238

还有 goog.dom.forms 里面多个方法的实现:
http://code.google.com/p/closure-library/source/browse/trunk/closure/goog/dom/forms.js

Closure 为了保障表单能正常提交(比如无 JS 也能工作),不能也不会在编译时重命名表单域的 name 。

同时查了一下起源:

form.name,从 DOM1就存在了,往后为了兼容也一直保留了:

The FORM element encompasses behavior similar to a collection and an element. It provides direct access to the contained input elements as well as the attributes of the form element.

DOM1
http://www.w3.org/TR/REC-DOM-Level-1/level-one-html.html#ID-40002357

DOM2
http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-40002357

form.elements 从 DOM2 开始有了:
https://developer.mozilla.org/en/DOM/form.elements

@ambar
Copy link
Author

ambar commented Apr 28, 2012

收获总结

问题根源是 Closure Events 的实现有问题,不应该假定 DOM 对象上有一个属性,而且假定那个属性(无论使用如何怪异的命名)会毫无冲突,这是完全可以避免的

对象添加属性的手段称为 expando,我认为至少要做到几点:

  • 普通的开发人员,任何时候不应该往 DOM 对象上添加自定义属性 —— 最著名的影响是添加一个动态对象到 DOM 属性会可能会引起内存泄漏。
  • 任何 JS 库都应该只往 DOM 对象添加一个属性,还要保障这个属性是 unique 的,而且这个属性只能使用纯粹的字符串,防止内存泄漏的危害。
  • 往 DOM 对象关联其他对象时,只能通过这个 expando 属性做为键的隐藏对象来操纵,比如 jquery 的一些 cache、data、event 就是通过这个属性来关联到 DOM 对象的。

Closure 已经往 DOM 对象添加了 element[goog.UID_PROPERTY_] ,它还要去检测 element.cutomEvent_ 属性,这就是错误所在:

  • 编译之后,天知道 customEvent_ 这个属性会变成了什么名字。
  • 假设我还使用了另一个库,比如 Yahoo Closure, 假如它也往 DOM 对象添加了一个该死的 customEvent_ 属性又该怎么办?

因为,我认为好的解决办法是通过内部 expando 属性来判断这个行为,比如:

internel_cache = {}
var isCustomEvents_ = internel_cache[ element[goog.UID_PROPERTY_] ].customEvent_
if (isCustomEvents_) then ...

比较差一点的办法就是忽略其他库的共存,比如:

// 严格类型检测
if ( element.cutomEvent_ ==== true) then ...
// 或,禁止编译此属性
if ( element['customEvent_] ) then ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment