打印

SD9032: IE6 IE7 IE8 IE9(Q) 中表单元素常见事件不产生事件冒泡

作者:钱宝坤

标准参考

表单元素常用事件有 change、select、submit、reset,他们在 Document Object Model Events 规范中均被标注为可冒泡(Bubbles: Yes)。

问题描述

IE6 IE7 IE8 IE9(Q) 中 change、select、submit、reset 事件均不产生事件冒泡。

造成的影响

如果将事件处理委托给产生这些事件的父元素或祖先元素处理,在 Chrome Safari Firefox 中均是没有问题的。但是由于 IE6 IE7 IE8 中这些事件不产生事件冒泡,将会导致位于祖先元素的事件委托没有被执行,可能会导致错误数据提交或页面UI不正常等情况出现。

受影响的浏览器

IE6 IE7 IE8 IE9(Q)  

问题分析

最早的 DOM Level 2 Event Model 版本为 1999 年 3 月成文,变更至今日均指定了 change、select、submit、reset 事件均可产生事件冒泡:

select
The select event occurs when a user selects some text in a text field. This event is valid for INPUT and TEXTAREA elements.
  • Bubbles: Yes
  • Cancelable: No
  • Context Info: None
change
The change event occurs when a control loses the input focus and its value has been modified since gaining focus. This event is valid for INPUT, SELECT, and TEXTAREA. element.
  • Bubbles: Yes
  • Cancelable: No
  • Context Info: None
submit
The submit event occurs when a form is submitted. This event only applies to the FORM element.
  • Bubbles: Yes
  • Cancelable: Yes
  • Context Info: None
reset
The reset event occurs when a form is reset. This event only applies to the FORM element.
  • Bubbles: Yes
  • Cancelable: No
  • Context Info: None

规范的成文时间已经涵盖了 IE6 开发时间,因此可以基本推断早期的 IE 版本如果遵循了此规范,那么事件将会冒泡到祖先元素上。

事实是否如此呢?我们看一组测试用例:

<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
    var addEvent = (document.addEventListener)
        ? (function(el, type, fn) {
            el.addEventListener(type, fn, false);
          })
        : (function(el, type, fn) {
            el.attachEvent('on' + type, fn)
          });
      
  var stopDefault = function(e) {
    (window.event)
      ? window.event.returnValue = false
      : e.preventDefault();
  };
  
  var output = function (msg) {
    pElement.innerHTML += msg + '<br />';
  };
  
  var addOutputMessageByTargets = function(targets, targetNames, targetEventNames) {
    for (var i = 0, c = targets.length; i < c; ++i) {
      for (var j = 0, len = targetEventNames.length; j < len; ++j) {
        addEvent(targets[i], targetEventNames[j], 
          (function(targetName, eventName) {
            return function(event) {
              if (targetName === 'HTMLFormElement' &&
                eventName === 'submit') { 
                stopDefault(event);
              }
              output(targetName + ' triggered ' + 
                  eventName + ' event.');
            }
          })(targetNames[i], targetEventNames[j])
        );
      }
    }
  };
  
  var pElement = document.getElementsByTagName('p')[0];
  var divElement = document.getElementsByTagName('div')[0];    
  var formElement = document.getElementsByTagName('form')[0];
  var inputTextElement = document.getElementsByTagName('input')[0];
  var inputCheckboxElement = document.getElementsByTagName('input')[1];
  var inputRadioElement = document.getElementsByTagName('input')[2];
  var selectElement = document.getElementsByTagName('select')[0];
  var textareaElement = document.getElementsByTagName('textarea')[0];
  var clearMessageElement = document.getElementsByTagName('button')[0]; 

  addOutputMessageByTargets(
    [window, document, document.body, divElement],
    ['DOMWindow', 'Document', 'HTMLBodyElement', 'HTMLDivElement'],
    ['submit', 'reset', 'change', 'select']
  );

  addOutputMessageByTargets(
    [formElement],
    ['HTMLFormElement'],
    ['submit', 'reset']
  );

  addOutputMessageByTargets(
    [
      inputTextElement, 
      inputCheckboxElement, 
      inputRadioElement,
      selectElement, 
      textareaElement
    ],
    [
      'HTMLInputElement type is text', 
      'HTMLInputElement type is checkbox', 
      'HTMLInputElement type is radio', 
      'HTMLSelectElement', 
      'HTMLTextareaElement'
    ],
    ['change']
  );
  
  addOutputMessageByTargets(
    [inputTextElement, selectElement, textareaElement],
    [
      'HTMLInputElement type is text', 
      'HTMLSelectElement', 
      'HTMLTextareaElement'
    ],
    ['select']
  );
  
  addEvent(clearMessageElement, 'click', function() {
    pElement.innerHTML = '';
  })
};
</script>
</head>
<body>
<div>
  <h3>Place change From: </h3>
  <form>
    <input type="text"/> <br />
    <input type="checkbox" /> <br />
    <input type="radio" name="radio"/> <br />
    <select> 
      <option>1</option>
      <option>2</option>
    </select> <br />
    <textarea></textarea> <br />
    <input type="submit" value="submit" />
    <input type="reset" value="reset"/> 
  </form>
</div>
<h3>output message: </h3>
<p></p>
<button>clear message</button>
</body>
</html>

用例中,我们将这些表单事件依次委托绑定给他们的父元素 DIV、祖先元素 BODY、以及位于事件冒泡顶层的 window 与 document 对象。

如果事件可冒泡,则我们将看到 DIV、BODY、document 与 window 均会在输出事件触发信息。反之,则可看到,仅触发事件的元素自身发出了事件消息。同时,还可以根据消息输出判断出是否有事件没有按照 DIV、BODY、Document、Window 的轨迹向上冒泡执行。

各浏览器中事件冒泡表现如下:

  IE6 IE7 IE8 IE9(Q)1 IE9(S)2 Firefox Chrome Safari Opera
change 事件 不冒泡 可冒泡至 window
select 事件 不冒泡 可冒泡至 window
submit 事件 不冒泡 可冒泡至 window
reset 事件 不冒泡 可冒泡至 window

【注1】:IE9(Q) 基本上是模拟 IE5.5 的整体表现方式,因此同样没有遵循规范中指定的事件冒泡规则。

【注2】:IE10 平台预览版第二版的标准文档模式与 IE9(S) 表现一致,事件均可冒泡,由于此篇成文时为非正式版本,故仅做提示而不入上表。

如上表所示,IE6 IE7 IE8 IE9(Q) 版本浏览器中 change、select、submit、reset 事件均不产生事件冒泡,导致其冒泡路径上的事件委托均没有被正常执行。

此现象说明 IE6 IE7 IE8 IE9(Q) 的 change、select、submit、reset 事件事实上都没有参照规范定义产生事件冒泡。

【注】:如果仅从直觉上来判断 IE6-8 的此部分处理是符合使用者预期的,这些事件应如同 focus、blur 事件一样不产生冒泡更合理。但是,考虑到可以触发这些事件的元素基本上都不可以被嵌套,那么规范如此定义将会为事件处理带来更大的灵活性。

解决方案

为了兼容低版本的 IE 浏览器,建议 change、select、submit、reset 事件均不要依赖事件冒泡机制委托给其祖先元素处理。

参见

知识库

相关问题

测试环境

操作系统版本: Windows 7 Ultimate build 7600
浏览器版本: IE6
IE7
IE8
IE9
Firefox 6.0
Chrome 16.0.891.0 dev-m
Safari 5.1(7534.50)
Operea 11.51
测试页面: form_elements_event_bubbles_test.html
本文更新时间: 2011-09-27

关键字

change select submit reset input textarea form 表单 表单元素 事件委托 事件冒泡