更优雅的事件触发兼容


Posted in Javascript onOctober 24, 2011

问题种种

做底层接口兼容,无非就是利用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

Javascript 相关文章推荐
javascript实现动态增加删除表格行(兼容IE/FF)
Apr 02 Javascript
让js弹出窗口居前显示的实现方法
Jul 10 Javascript
jquery中的查找parents与closest方法之间的区别
Dec 02 Javascript
jquery选择器之基本过滤选择器详解
Jan 27 Javascript
javascript中的 object 和 function小结
Aug 14 Javascript
JS判断键盘是否按的回车键并触发指定按钮点击操作的方法
Feb 13 Javascript
socket.io学习教程之基础介绍(一)
Apr 29 Javascript
基于Vue.js 2.0实现百度搜索框效果
Dec 28 Javascript
对vue2.0中.vue文件页面跳转之.$router.push的用法详解
Aug 24 Javascript
jQuery pager.js 插件动态分页功能实例分析
Aug 02 jQuery
微信小程序上传帖子的实例代码(含有文字图片的微信验证)
Jul 11 Javascript
vue route新窗口跳转页面并且携带与接收参数
Apr 10 Vue.js
myEvent.js javascript跨浏览器事件框架
Oct 24 #Javascript
最佳的addEvent事件绑定是怎样诞生的
Oct 24 #Javascript
关于javascript function对象那些迷惑分析
Oct 24 #Javascript
文本框根据输入内容自适应高度的代码
Oct 24 #Javascript
js创建数据共享接口——简化框架之间相互传值
Oct 23 #Javascript
javascript模版引擎-tmpl的bug修复与性能优化分析
Oct 23 #Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
Oct 23 #Javascript
You might like
实用函数3
2007/11/08 PHP
Fatal error: Call to undefined function curl_init()解决方法
2010/04/09 PHP
windows下zendframework项目环境搭建(通过命令行配置)
2012/12/06 PHP
在PHP中设置、使用、删除Cookie的解决方法
2013/05/06 PHP
深入探讨:Nginx 502 Bad Gateway错误的解决方法
2013/06/03 PHP
身份证号码前六位所代表的省,市,区, 以及地区编码下载
2007/04/12 Javascript
基于jquery的鼠标拖动效果代码
2012/05/30 Javascript
利用JQuery和Servlet实现跨域提交请求示例分享
2014/02/12 Javascript
jquery获取radio值实例
2014/10/16 Javascript
jquery实现页面百叶窗走马灯式翻滚显示效果的方法
2015/03/12 Javascript
举例详解AngularJS中ngShow和ngHide的使用方法
2015/06/19 Javascript
jQuery实现鼠标悬停3d菜单展开动画效果
2017/01/19 Javascript
koa2使用ejs和nunjucks作为模板引擎的使用
2018/11/27 Javascript
解决Layui当中的导航条动态添加后渲染失败的问题
2019/09/25 Javascript
jQuery实现轮播图源码
2019/10/23 jQuery
原生js实现自定义滚动条
2021/01/20 Javascript
[01:14:34]DOTA2上海特级锦标赛C组资格赛#2 LGD VS Newbee第一局
2016/02/28 DOTA
[01:11:21]DOTA2-DPC中国联赛 正赛 VG vs Elephant BO3 第一场 3月6日
2021/03/11 DOTA
Python 中pandas.read_excel详细介绍
2017/06/23 Python
Python爬虫实战:分析《战狼2》豆瓣影评
2018/03/26 Python
Pandas DataFrame 取一行数据会得到Series的方法
2018/11/10 Python
python七夕浪漫表白源码
2019/04/05 Python
python  logging日志打印过程解析
2019/10/22 Python
使用Matplotlib绘制不同颜色的带箭头的线实例
2020/04/17 Python
Python数组拼接np.concatenate实现过程
2020/04/18 Python
css3media响应式布局实例
2016/07/08 HTML / CSS
专科毕业生学习生活的自我评价
2013/10/26 职场文书
给领导的致歉信范文
2014/01/13 职场文书
喜之郎果冻广告词
2014/03/20 职场文书
2014年党务公开方案
2014/05/08 职场文书
学生顶撞老师的检讨书
2014/09/17 职场文书
法务专员岗位职责
2015/02/14 职场文书
教育教学读书笔记
2015/07/02 职场文书
教师节主题班会教案
2015/08/17 职场文书
股权投资协议书
2016/03/23 职场文书
高中16字霸气押韵班级口号集锦!
2019/06/27 职场文书