前の記事では、jQuery 1.4 バージョンにおけるいくつかの新しいブラウザー機能検出ソリューションとその特定の目的を紹介しました。この記事ではイベントに焦点を当て、比較的完全で汎用的なイベント検出ソリューションを紹介します。
イベント検出は、さまざまなブラウザーでイベントが存在するかどうかを検出することです。これは、JavaScript を作成するプロセスでも非常に重要ですが、mouseenter/mouseleave イベントは実用的ではありません。すべてのブラウザで標準サポートが提供されているため、手動でシミュレートする必要があります。
function addEvent(element, name, handler) { if (name == 'mouseenter' && !hasEvent(name, element)) { //通过其他手段模拟mouseenter事件 } //正常的事件注册 };
この記事では、上記のコード内の hasEvent の特定の実装に焦点を当てます。
最も基本的なイベントの検出方法としては、イベント登録方法から始める必要があります。
通常、イベントを登録するには 3 つの方法があり、その 1 つはインラインです。つまり、HTML の属性を通じてイベントを宣言します。
<button onclick="alert('CLICKED!');">CLICK ME</button>
上記のコードはボタンを作成します。タグを付けてクリックイベントを登録しました。
もう 1 つの解決策は、onclick に値を直接割り当ててイベントを登録することです。
document.getElementById('myButton').onclick = function() { alert('CLICKED!'); };
イベントを登録する上記の 2 つの方法から、onclick は実際にはbuttonタグ)に値を代入することでイベントの登録が完了します。
したがって、最も基本的なイベント検出ソリューションは、DOM 要素に on[event name] 属性が存在するかどうかを確認することであるため、最も単純なバージョンがあります:
function hasEvent(name, element) { name = name.indexOf('on') ? 'on' + name : name; element = element || document.createElement('p'); var supported = name in element; };
必須events は on[イベント名] という形式で要素の属性として存在するため、汎用性の観点から必要に応じてイベント名に on を追加するだけで済みます。また、イベントの有無を判定する一般的な関数ですので、特定の要素を指定しない場合は、最もよく使われているp要素を代用することができます。
一部のイベントは一部の要素に固有であり、通常は次のものが含まれます:
フォーム固有のイベント: submit、reset
固有イベントの入力: 変更、選択
固有イベントの画像: ロード、エラー、中止
これらのイベントの存在を考慮すると、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 のような属性がある理由は、JavaScript の弱い型メカニズムにより、この属性が DOM 要素オブジェクトの __proto__ にあるためです。 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'; }
へ これまでのところ、ほとんどのブラウザと互換性を保ちながら各 DOM 要素のイベントを検出することは可能ですが、ウィンドウ オブジェクトのイベント検出については完全な解決策はありません。
IE系、Chrome、Safariの場合は、windowの単純な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函数的实现方式。