이전 기사에서는 jQuery 1.4 버전의 몇 가지 새로운 브라우저 기능 감지 솔루션과 구체적인 목적을 소개했습니다. 이 기사에서는 이벤트에 중점을 두고 비교적 완전하고 보편적인 이벤트 감지 솔루션을 소개합니다.
이벤트 감지는 다양한 브라우저에서 이벤트가 존재하는지(사용 가능한지) 감지하는 것입니다. 이는 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>
위 코드는 버튼을 생성합니다. 태그를 지정하고 클릭 이벤트를 등록했습니다.
또 다른 해결 방법은 onclick에 직접 값을 할당하여 이벤트를 등록하는 것입니다.
document.getElementById('myButton').onclick = function() { alert('CLICKED!'); };
위의 두 가지 이벤트 등록 방법에서 onclick이 실제로 버튼 태그)에 값을 지정하면 이벤트 등록이 완료됩니다.
따라서 가장 기본적인 이벤트 감지 솔루션은 on[이벤트 이름] 속성이 DOM 요소에 존재하는지 확인하는 것이므로 가장 간단한 버전이 있습니다.
function hasEvent(name, element) { name = name.indexOf('on') ? 'on' + name : name; element = element || document.createElement('p'); var supported = name in element; };
필수 이벤트는 on[이벤트 이름] 형태로 요소의 속성으로 존재하므로, 범용성 측면에서 필요할 경우 이벤트 이름에 'on'만 추가하면 됩니다. 또한, 이벤트 발생 여부를 판단하는 일반적인 함수이므로, 특정 요소가 지정되지 않은 경우에는 가장 널리 사용되는 p 요소를 대신 사용할 수 있다.
일부 이벤트는 일부 요소에 고유하며 일반적으로 다음을 포함합니다.
양식 고유 이벤트: 제출, 재설정
고유 이벤트 입력: 변경, 선택
img 고유 이벤트: 로드, 오류, 중단
이러한 이벤트의 존재를 고려하면 p 요소를 사용하면 잘못된 결과가 나올 수 있으므로 범용 대체 요소를 만들 때 사전을 사용하여 만들어야 하는 요소 태그 이름을 유지할 수 있습니다.
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] || 'p'); supported = name in element; } })();
클로저를 사용하여 태그를 정적 사전으로 사용하면 객체 생성 비용을 어느 정도 줄일 수 있습니다.
DOM 요소에 onclick과 같은 속성이 있는 이유는 DOM 요소 객체의 __proto__에 이 속성이 있기 때문입니다. Javascript의 약한 유형 메커니즘으로 인해 외부 코드가 pass __proto__에 속성을 추가하면 hasEvent 함수의 결과에 영향을 미칩니다. 예를 들어 다음 코드는 Firefox 및 Chrome에서 잘못된 결과를 생성합니다.
document.createElement('p').__proto__.ontest = function() {}; var supported = hasEvent('test', document.createElement('p')); //true
위 예에서는 호출할 때 __proto__ 속성이 수정됩니다. hasEvent에는 다른 p 객체가 사용되지만 __proto__의 본질은 프로토타입 체인의 객체이므로 모든 p 객체에 영향을 미칩니다.
이 상황을 처리하려면 __proto__ 속성에서 해당 속성을 삭제해야 합니다. 기본 유형 속성은 DontDelete로 표시되어 있으므로 delete 키워드를 사용하여 삭제할 수 없으므로 hasEvent 함수 다음 로직을 추가하면 더욱 안전한 판단을 내릴 수 있습니다.
var temp; if (supported && (temp = proto[name]) && delete proto[name]) { supported = name in element; proto[name] = temp; }
로직은 매우 간단합니다. __proto__에 추가될 수 있는 항목을 삭제하고 다시 시도해 보세요. 원래 값을 추가하면 값이 다시 변경됩니다.
안타깝게도 위에 제공된 hasEvent 함수는 Firefox에서 완벽하게 작동하지 않습니다. Firefox에서 다음 코드를 실행하면 잘못된 결과가 나타납니다.
alert('onclick' in document.documentElement); //Firefox弹出false
따라서 Firefox를 지원하려면 hasEvent 함수를 다시 수정해야 합니다. 대부분의 브라우저에서 요소가 인라인으로 이벤트를 등록하면 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[이벤트 이름] 속성에 함수를 추가한 후 함수 객체를 얻었는지 확인합니다.
따라서 위에 제공된 메소드에서 hasEvent 함수가 false를 반환하는 경우 다음 추가 코드를 추가하여 이벤트가 존재하는지 추가로 확인할 수 있습니다.
if (!supported) { element.setAttribute(name, 'return;'); supported = typeof element[name] == 'function'; }
to 지금까지는 대부분의 브라우저와 호환되면서 각 DOM 요소의 이벤트 감지가 가능하지만, 윈도우 객체의 이벤트 감지에 대한 완전한 솔루션은 없습니다.
IE 시리즈, Chrome, Safari의 경우 창에서 간단한 on[이벤트 이름]을 사용하여 이벤트 존재 여부를 감지할 수 있으므로 DOM 오염을 방지하는 원래의 hasEvent 함수를 사용하면 작업을 잘 완료할 수 있습니다.
Firefox에서만 다음 코드는 잘못된 결과를 제공합니다.
alert('onload' in window); //Firefox弹出false alert('onunload' in window); //Firefox弹出false alert('onerror' in window); //Firefox弹出false
다행스럽고도 터무니없는 점은 Firefox가 p 3 이벤트와 같은 요소에서 위의 내용을 이상하게 감지할 수 있다는 것입니다. 일반 DOM 요소에서 이벤트를 감지하는 데 직접 오류가 발생하고 창에서 이벤트를 감지할 수도 있습니다. 다행히 대부분의 개발자는 p와 같은 요소에 언로드 이벤트가 있는지 확인하지 않습니다. 따라서 일부 이벤트를 감지하기 위해 창의 이벤트를 p 객체로 전달하기 위해 hasEvent 함수가 추가됩니다.
if (!supported) { if (!element.setAttribute || !element.removeAttribute) { element = document.createElement('p'); } element.setAttribute(name, 'return;'); supported = typeof element[name] == 'function'; element.removeAttribute(name); }
이 시점에서 비교적 완전한 hasEvent 함수가 완성되었지만 여전히 몇 가지 문제가 있습니다. 다음 코드와 같은 Firefox:
alert(hasEvent('unload', document.createElement('p')); //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] || 'p'); 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,但是p有unload事件(OTL) if (!supported) { if (!element.setAttribute || !element.removeAttribute) { element = document.createElement('p'); } element.setAttribute(name, 'return;'); supported = typeof element[name] == 'function'; element.removeAttribute(name); } //添加到缓存 cache[name] = supported; return supported; }; })();
Mutation Event是由DOM Level 2制定的一类特殊的事件,这些事件在某个元素为根的DOM树结构发生变化时触发,可以在这里看到具体的事件列表。
遗憾的是hasEvent函数无法检测到Mutation Event,因此对于此类事件,需要另一种较为复杂的事件检测方案。
从Mutation Event的列表中可以发现,此类事件的特点在于当DOM树结构发生变化时才会被触发,因此可以使用下面这套逻辑去检测:
准备一个标记位,默认为false。
创建出一个DOM树结构。
注册一个Mutation Event。
通过一定手段让这个DOM树变化,从而触发注册的事件。
在事件处理函数中,将标记位设为true。
返回标记位。
具体的实现代码可以如下:
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', 'p', function (p) { p.id = 'new'; });
对于其他事件的检测,同样只需要制作出一个特定的change函数即可。
这个事件在文档加载完成时触发,但不需要等待图片等资源下载,多数Javascript框架的document.ready都会试图使用这个事件。
无论是hasEvent函数还是hasMutationEvent函数都无法检测到这个事件,但是问题不大,因为:
这事件和onload一样,页面的生命周期中只会触发一次,不会频繁使用。
所有支持addEventListener的浏览器都支持这个事件(包括IE9),因此判断简单。
所以这个事件被排除在了本文讨论范围之外,具体的可以查看各框架的document.ready函数的实现方式。