更优雅的事件触发兼容 |
本文标签:事件触发,兼容 问题种种 做底层接口兼容,无非就是利用if,判断客户端支持哪个接口的问题 。最著名的例子就是事件: 复制代码 代码如下: var addEvent = function(e, what, how) { if (e.addEventListener) e.addEventListener(what, how, false) else if (e.attachEvent) e.attachEvent(on + what, how) } 这里考虑了给元素绑定事件时可能遇到的两种状况——标准的W3C DOM接口以及DHTML提供的接口 。当然这个例子还很粗糙,但足够说明问题了 。 原先的方法是在兼容层调用有现场判断并进入相应的if分支 。很显然,这种“现场判断”的方法效率并不高 。后来,人们采用这样的办法: 复制代码 代码如下: if (MSIE) { addEvent = function(e, what, how) { e.attachEvent(on + what, how); } } else { addEvent = function(e, what, how) { e.addEventListener(what, how); } } 在一次判断后给addEvent绑定不同的代码,从而免去了运行时的分支判断 。 很可惜,这个问题也不小 。首先把“采用attachEvent”和“客户端是MSIE”绑定在一起是个很过时的想法 。假如微软哪天良心发现了怎么办?这事情现在就发生了——IE9明确支持了DOM接口,甚至DOM3都支持 。结果,就这个“良心发现”的举动会毁掉许多前端库,他们必须被迫修改代码(如同IE8来时那样) 。况且这种做法没有考虑“未知的客户端”——据我所知,Google发布Chrome后也导致不少类库重写代码 。 特性检测 那究竟该怎么做?特性检测就可以最大限度地避免“新客户端”带来的麻烦——通过一组在类库初始化时定义的代码来检测客户端拥有的特性,并利用这一组检测值绑定类库代码: 复制代码 代码如下: var supportsAddEventListener = !!(checkerElement.addEventListener); if (supportsAddEventListener) { addEvent = function(e, what, how) { e.addEventListener(what, how); } } else if (supportsAttachEvent) { addEvent = function(e, what, how) { e.attachEvent(on + what, how); } } 特性检测实际上是将“使用某个客户端”和“支持某个特性”进行解耦——让if分支直接针对“特性有无”(接口是否一致)判断,从而消除客户端制造商“良心发现”造成的“好心办坏事” 。事实上这么做也是符合历史潮流之选——当标准接口逐渐普及,客户端之间渐渐“表征一致”时,为什么不做个一致的兼容层接口呢? 跌落 让我们重新看看这些代码 。通常,一条利用特性检测进行兼容的代码往往是这样: 复制代码 代码如下: if (new_interface_detected) { comp = function() {uses_new_interface}; } else if (old_interface_detected) { comp = function() {uses_old_interface}; } else { throw new Error(Unadaptable!) } 换言之,过程是: 如果客户端支持新接口,就将兼容层绑定到新接口上 否则,如果客户端支持老接口/不一致接口,就将兼容层绑定到老接口上 否则,如果可以的话,给出错误回馈 亦即,兼容层程序是从高空“掉”下来,如果客户端支持“高级”特性(新接口、标准接口)就将它“接住”——兼容层就有了归宿;否则继续向下掉——哦,老接口接住了,就用老接口;如果一直没人接住,于是——啪——摔倒了地上,并且用最后一口气喊一声:“你用的客户端太小众,我拿你没办法了!” 这和什么比较像? 事实上,如果你了解JavaScript对象系统的机理,你就可以类比:这不就是原型嘛!原型系统就是利用了这种跌落——寻找某个成员,如果它在这个对象里定义了,就返回之;否则沿着原型链向上搜(没错,这次是向上的),如此重复,直到真的连原型链都到头的时候,返回个undefined 。 说做就做!这里同样用addEvent为例 。首先,我们定义一个空驱动,它里面什么都不包含: var nullDriver = {} 然后,就是创建个对象,并且把原型链指向它 。在ECMA V5时代,我们可以用Object.create,可惜,现在还有N多老客户端(否则做什么兼容啊),所以自己craft个函数: 复制代码 代码如下: var derive = Object.create ? Object.create: function() { var T = function() {}; return function(obj) { T.prototype = obj; return new T } }() 这个用法你可能会觉得很诡异,但它工作起来一点问题没有,速度也不慢——能达到Object.create的一半 。我们就用这个derive开动: 复制代码 代码如下: var dhtmlDriver = derive(nullDriver); var dhtmlDriverBugfix = derive(dhtmlDriver); 这里的bugfix是针对一些“bug”和特殊情况定义的特别Driver 。这里你可以忽略它 。好了,DHTML里面addEvent是什么来着? 复制代码 代码如下: if (supportsAttachEvent) { dhtmlDriver.addEvent = function(e, what, how) { e.attachEvent(on + what, how) } } 然后呢?位于原型链最前端的应该是W3C的标准驱动啊,写上! 复制代码 代码如下: var w3cDriver = derive(dhtmlDriverBugfix); var w3cDriverBugfix = derive(w3cDriver); if (supportsAddEventListener) { w3cDriver.addEvent = function(e, what, how) { e.addEventListener(what, how) } } 最后,我们就放个东西上去做最后调用的接口 。(因为w3cDriverBugfix太难看……) var driver = derive(w3cDriverBugfix); 然后就调用好了 。看,这就让那些长得吓人的分支判断变得简单有效,但不失fallback本色:在支持addEventListener上调用addEvent等价于调用w3cDriver.addEvent,而在不支持addEventListener的客户端上就会跌落到底下,比如调用dhtmlDriver.addEvent 。另外,进行bugfix也很容易——可以在专门的“bugfix”层进行hook,而原有层丝毫不受影响 。 等等,继承这么多层 会很慢么?诚然,那么深的原型链肯定会慢,不过我有办法 。还记得给对象的属性写入时会发生什么事情吗? 复制代码 代码如下: var ego = function(x) {return x} for (var each in driver) { if (! (each in nullDriver)) { driver[each] = ego(driver[each]) } } 没错,原来高企在原型链上面的方法会“哗”的一下掉到最下面!这回不用沿着原型链向上搜了,直接从最底端获取属性即可 。这里用ego函数的原因是防止一些浏览器“优化掉”这里的代码 。 总结 虽然这里谈兼容,可是,它的精华却在语言特性上——利用原型继承,我们可以很优雅地完成这个令人头疼的操作 。是的,框架的美感不应该只在外表,其内部——即使是最最令人烦的内部——也同样要优雅 。 这里的技术可以在dess中找到 。 来自:typeof.net |