细说浏览器特性检测(2)-通用事件检测


Posted in Javascript onNovember 05, 2010

事件检测,即检测某一事件在不同的浏览器中是否存在(可用),这在编写Javascript的过程中也非常重要,如mouseenter/mouseleave事件虽然实用,但并不是所有浏览器都提供了标准的支持,因此需要自己手动模拟,即:

function addEvent(element, name, handler) { 
if (name == 'mouseenter' && !hasEvent(name, element)) { 
//通过其他手段模拟mouseenter事件 
} 
//正常的事件注册 
};

本文就重点讲述以上代码中hasEvent的具体实现。

基本方案

关于事件的最基本检测方式,则需要从事件的注册方法开始说。

事件通常有3种注册方式,其中之一就是内联式,即在HTML中通过属性的方式声明事件,比如:

<button onclick="alert('CLICKED!');">CLICK ME</button>

以上代码创建了一个button标签,并注册了click事件。

另一个方案是通过直接给onclick赋值来注册事件:

document.getElementById('myButton').onclick = function() { 
alert('CLICKED!'); 
};

从上面两种注册事件的方式可以发现,其实onclick是button标签的一种属性(attribute),通过对其赋值可以完成事件的注册。

因此,最基本的事件检测方案,就是通过检查on[事件名]属性是否存在于DOM元素之中,因此有最简单的一个版本:

function hasEvent(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
element = element || document.createElement('div'); 
var supported = name in element; 
};

需要注意的是,事件是对on[事件名]的形式作为元素的属性而存在的,因此从通用性上考虑,在必要的时候对事件名补上'on'即可。另外由于是一个通用的判断事件是否可用的函数,当没有给定具体的元素时,可以使用最广泛应用的div元素作为替代。

部分标签特有事件

有些事件是一些元素特有的,通常包括以下几个:

  • form独有事件:submit、reset
  • input独有事件:change、select
  • img独有事件:load、error、abort

考虑到这些事件的存在,使用div元素有时会得到错误的结果,因此在创建一个通用的替代用元素时,可以使用一个字典来维护需要创建的元素标签名:

var hasEvent = (function() { 
var tags = { 
onsubmit: 'form', onreset: 'form', 
onselect: 'input', onchange: 'input', 
onerror: 'img', onload: 'img', onabort: 'img' 
}; 
return function(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
element = element || document.createElement(tags[name] || 'div'); 
supported = name in element; 
} 
})();

使用闭包将tags作为静态的字典使用,可以在一定程度上减少对象生成的开销。

DOM污染

DOM元素之所以会有类似onclick的属性,是因为在DOM元素对象的__proto__中有这个属性,由于Javascript弱类型机制,外部代码可以通过对__proto__添加属性而影响hasEvent函数的结果,如以下代码在Firefox和Chrome中就会产生错误的结果:

document.createElement('div').__proto__.ontest = function() {}; 
var supported = hasEvent('test', document.createElement('div')); //true

在上面的示例中,虽然在修改__proto__属性和调用hasEvent时,使用的是不同的div对象,但由于__proto__的实质是原型链中的对象,因此会影响到所有的div对象。

为了处理这种情况,需要尝试将__proto__属性中相应的属性进行删除,由于原生类型的属性带有DontDelete标记,是无法使用delete关键字进行删除的,因此对hasEvent函数附加以下的逻辑就可以更安全地判断:

var temp; 
if (supported && (temp = proto[name]) && delete proto[name]) { 
supported = name in element; 
proto[name] = temp; 
}

逻辑很简单,尝试把__proto__中有可能附加上去的删了再试一试,当然别忘了再把原来的值变回去。

Firefox开始BUG

很遗憾,前文提供的hasEvent函数并不能在Firefox完美工作,在Firefox中运行以下代码将得到false的结果:

alert('onclick' in document.documentElement); //Firefox弹出false

因此,需要再次改造hasEvent函数以支持Firefox。在多数浏览器中,当元素使用内联方式注册了事件之后,可以通过element.on[事件名]来获取注册在上面的函数对象,例如:

<button id="test" onclick="alert('CLICKED!');" ontest="alert('TEST!');">CLICK ME</button> 
<script type="text/javascript"> 
var button = document.getElementById('test'); 
alert(typeof button.onclick); //弹出function 
alert(typoef button.ontest); //弹出string 
</script>

因此,只需要通过Javascript将一个表示函数的字符串挂载到on[事件名]属性(attribute)上,再去获取并判断是否得到了一个函数对象即可。

因此hasEvent函数在前文提供的方法返回false时,可以额外增加以下的代码以进一步确定事件是否存在:

if (!supported) { 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
}

Firefox继续BUG

到现在为止,已经可以在兼容多数浏览器的情况下检测各DOM元素的事件,但是对于window对象的事件检测还没有一个完整的方案。

对于IE系列、Chrome和Safari,都可以使用简单的on[事件名] in window检测事件是否存在,因此原有的提供防止DOM污染后的hasEvent函数可以很好地完成任务。

唯有Firefox上,以下代码会给出错误的结果:

alert('onload' in window); //Firefox弹出false 
alert('onunload' in window); //Firefox弹出false 
alert('onerror' in window); //Firefox弹出false

值得庆幸也值得愤怒的是,Firefox很诡异地可以在div等元素上检测到以上3个事件,这直接导致对普通DOM元素检测事件的错误,也导致我们可以检测到window上的事件。好在一般开发者也不会去一个div之类的元素上检测是否有unload事件。因此补充hasEvent函数,将window上的事件导向一个div对象来检测部分事件:

if (!supported) { 
if (!element.setAttribute || !element.removeAttribute) { 
element = document.createElement('div'); 
} 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
element.removeAttribute(name); 
}

至此,一个较为完整的hasEvent函数完成了,虽然在Firefox上还存在一些问题,比如以下的代码:

alert(hasEvent('unload', document.createElement('div')); //Firefox弹出true

但是在99%的应用场合之下,这个函数是可以正确的工作的。

添加缓存

为了进一步提高hasEvent的工作效率,考虑到DOM规范规定的事件数量不多,可以对通用的事件(即不指定检测的元素对象)检测添加缓存机制。

添加了缓存之后,最终完整的hasEvent函数如下:

var hasEvent = (function () { 
var tags = { 
onsubmit: 'form', onreset: 'form', 
onselect: 'input', onchange: 'input', 
onerror: 'img', onload: 'img', onabort: 'img' 
}, 
cache = {}; 

return function(name, element) { 
name = name.indexOf('on') ? 'on' + name : name; 
//命中缓存 
if (!element && name in cache) { 
return cache[name]; 
} 
element = element || document.createElement(tags[name] || 'div'); 
var proto = element.__proto__ || {}, 
supported = name in element, 
temp; 
//处理显示在元素的__proto__上加属性的情况 
if (supported && (temp = proto[name]) && delete proto[name]) { 
supported = name in element; 
proto[name] = temp; 
} 
//处理Firefox不给力的情况 
//Firefox下'onunload' in window是false,但是div有unload事件(OTL) 
if (!supported) { 
if (!element.setAttribute || !element.removeAttribute) { 
element = document.createElement('div'); 
} 
element.setAttribute(name, 'return;'); 
supported = typeof element[name] == 'function'; 
element.removeAttribute(name); 
} 
//添加到缓存 
cache[name] = supported; 
return supported; 
}; 
})();

Mutation Event

Mutation Event是由DOM Level 2制定的一类特殊的事件,这些事件在某个元素为根的DOM树结构发生变化时触发,可以在这里看到具体的事件列表。

遗憾的是hasEvent函数无法检测到Mutation Event,因此对于此类事件,需要另一种较为复杂的事件检测方案。

从Mutation Event的列表中可以发现,此类事件的特点在于当DOM树结构发生变化时才会被触发,因此可以使用下面这套逻辑去检测:

  1. 准备一个标记位,默认为false。
  2. 创建出一个DOM树结构。
  3. 注册一个Mutation Event。
  4. 通过一定手段让这个DOM树变化,从而触发注册的事件。
  5. 在事件处理函数中,将标记位设为true。
  6. 返回标记位。

具体的实现代码可以如下:

function hasMutationEvent(name, tag, change) { 
var element = document.createElement(tag), 
supported = false; 
function handler() { 
supported = true; 
}; 
//IE9开始支持addEventListener,因此只有IE6-8没有这个函数 
//但是IE6-8已经确定不支持Mutation Event,所以有这个判断 
if (!element.addEventListener) { 
return false; 
} 
element.addEventListener(name, handler, false); 
change(element); 
element.removeEventListener(name, handler, false); 
return supported; 
};

例如需要检测DOMAttrModified事件是否存在,只需要用以下代码:

var isDOMAttrModifiedSupported = 
hasMutationEvent('DOMAttrModified', 'div', function (div) { div.id = 'new'; });

对于其他事件的检测,同样只需要制作出一个特定的change函数即可。

DOMContentLoaded

这个事件在文档加载完成时触发,但不需要等待图片等资源下载,多数Javascript框架的document.ready都会试图使用这个事件。

无论是hasEvent函数还是hasMutationEvent函数都无法检测到这个事件,但是问题不大,因为:

  1. 这事件和onload一样,页面的生命周期中只会触发一次,不会频繁使用。
  2. 所有支持addEventListener的浏览器都支持这个事件(包括IE9),因此判断简单。

所以这个事件被排除在了本文讨论范围之外,具体的可以查看各框架的document.ready函数的实现方式。

相关资源

  • Detecting event support without browser sniffing为本文提供了大量的思路。
  • Diego Perini's NWMatcher提供了Mutation Event检测的思路。
  • 点此查看hasEvent和hasMutationEvent的源码。

哪位无聊就把所有的Mutation Event的检测函数写出来吧……

Javascript 相关文章推荐
js添加table的行和列 具体实现方法
Jul 22 Javascript
jquery如何实现锚点链接之间的平滑滚动
Dec 02 Javascript
使用jQuery判断IE浏览器版本的代码
Jun 14 Javascript
比较常见的javascript中定义函数的区别
Nov 09 Javascript
Bootstrap的fileinput插件实现多文件上传的方法
Sep 05 Javascript
jQuery实现别踩白块儿网页版小游戏
Jan 18 Javascript
Bootstrap BootstrapDialog使用详解
Feb 17 Javascript
学习使用Bootstrap栅格系统
May 11 Javascript
vue将单页面改造成多页面应用的方法
Nov 25 Javascript
微信小程序wx.request拦截器使用详解
Jul 09 Javascript
微信小程序实现简单的select下拉框
Nov 23 Javascript
vue router 动态路由清除方式
May 25 Vue.js
需要做特殊处理的DOM元素属性的访问
Nov 05 #Javascript
基于jQuery的仿flash的广告轮播
Nov 05 #Javascript
jquery实现文本框鼠标右击无效以及不能输入的代码
Nov 05 #Javascript
基于jquery的loading效果实现代码
Nov 05 #Javascript
解决jQuery插件tipswindown与hintbox冲突
Nov 05 #Javascript
Jquery数独游戏解析(一)-页面布局
Nov 05 #Javascript
TinyMCE 新增本地图片上传功能
Nov 05 #Javascript
You might like
PHP strstr 函数判断字符串是否否存在的实例代码
2013/09/28 PHP
详解PHP中的状态模式编程
2015/08/11 PHP
php实现替换手机号中间数字为*号及隐藏IP最后几位的方法
2016/11/16 PHP
详解如何实现Laravel的服务容器的方法示例
2019/04/15 PHP
PHP Ajax跨域问题解决方案代码实例
2020/08/01 PHP
让焦点自动跳转
2006/07/01 Javascript
Javascript中暂停功能的实现代码
2007/03/04 Javascript
Javascript 的addEventListener()及attachEvent()区别分析
2009/05/21 Javascript
jquery无缝向上滚动实现代码
2013/03/29 Javascript
javascript贪吃蛇完整版(源码)
2013/12/09 Javascript
在父页面得到zTree已选中的节点的方法
2015/02/12 Javascript
JS+CSS实现滑动切换tab菜单效果
2015/08/25 Javascript
jQuery中get方法用法分析
2016/12/07 Javascript
Bootstrap滚动监听组件scrollspy.js使用方法详解
2017/07/20 Javascript
vue2.0 自定义 饼状图 (Echarts)组件的方法
2018/03/02 Javascript
VUE 全局变量的几种实现方式
2018/08/22 Javascript
详解npm 配置项registry修改为淘宝镜像
2018/09/07 Javascript
浅谈vue中$bus的使用和涉及到的问题
2020/07/28 Javascript
js实现贪吃蛇小游戏(加墙)
2020/07/31 Javascript
解决Mint-ui 框架Popup和Datetime Picker组件滚动穿透的问题
2020/11/04 Javascript
python中map()与zip()操作方法
2016/02/27 Python
利用Python为iOS10生成图标和截屏
2016/09/24 Python
[原创]Python入门教程1. 基本运算【四则运算、变量、math模块等】
2018/10/28 Python
使用Python3内置文档高效学习以及官方中文文档
2019/05/19 Python
Python 获取指定文件夹下的目录和文件的实现
2019/08/30 Python
python 插入日期数据到Oracle实例
2020/03/02 Python
Rockport乐步美国官网:风靡美国的白宫鞋
2016/11/24 全球购物
CSS代码检查工具stylelint的使用方法详解
2021/03/27 HTML / CSS
营销总监岗位职责范本
2014/02/26 职场文书
材料成型及控制工程专业求职信
2014/06/19 职场文书
城市规划应届生推荐信
2014/09/08 职场文书
2014年师德师风自我剖析材料
2014/09/27 职场文书
人大代表选举标语
2014/10/07 职场文书
朋友圈早安励志语录!
2019/07/08 职场文书
HR必备:超全面的薪酬待遇管理方案!
2019/07/12 职场文书
gtx1650怎么样 gtx1650显卡相当于什么级别
2022/04/08 数码科技