实现对onchange事件的事件代理是最为复杂的,在FF与最新版的opera中,它是能冒泡到顶层对象window;对于其他标准浏览器,由于它的事件监听器拥有三个参数,我们将最后一个设为true,实施捕获就一了百了;但对于IE就麻烦,既不能冒泡又不能使用捕获,唯一可行就是使用事件模拟,换言之,使用其他事件代替onchange的效果。jQuery动用了四种事件来模拟它,通过对它的深入研究,遂放弃它的设计,搞出自己的方案出来。
关键点有两个:
- 监听元素(组)的状态变化
- 使用何种事件充当伪onchange事件。
先解决第一个问题。能使用onchange事件的元素大抵有如下这些(暂时不考虑HTML5新增的)
<form id="aaa" > <select name="sweets" multiple="multiple" id="bbb" > <option>Chocolate</option> <option selected="selected">Candy</option> <option>Taffy</option> <option selected="selected">Caramel</option> <option>Fudge</option> <option>Cookie</option> </select><br> <input type="file"/><br/> <input type="radio" name="r" > <input type="radio" name="r" > <input type="radio" name="r" ><br> <input type="checkbox" name="ddd" > <input type="checkbox" name="ddd" ><br> <input value="文本域" id="eee" ><br> <textarea>文本区</textarea> </form>
要监听它们的状态,首先要知道它是什么样子,然后到我们这个时点又是什么样子。这个样子,可以通过对元素的value,checked,selected等进行比较。但jQuery犯了个错误,有些元素是一组取值才有意义,如下拉框(这个jQuery是对的),还有checkbox与radio。看下面实验。
<form action=""> <fieldset><legend>实验1</legend> <input type="radio" name="r" onclick="alert(this.checked)"> <input type="radio" name="r" onclick="alert(this.checked)"> <input type="radio" name="r" onclick="alert(this.checked)"><br> <input type="checkbox" name="ddd" onclick="alert(this.checked)"> <input type="checkbox" name="ddd" onclick="alert(this.checked)"><br> </fieldset> </form>
我们发现radio很独特,怎么点,它都是true,那岂不是不能区分它是否已发生变化吗?!我们换一种判定。
<form action=""> <fieldset><legend>实验2</legend> <input type="radio" name="gggg" onclick="getVal(this)"> <input type="radio" name="gggg" onclick="getVal(this)"> <input type="radio" name="gggg" onclick="getVal(this)"><br> <input type="checkbox" name="ddd2" onclick="getVal(this)"> <input type="checkbox" name="ddd2" onclick="getVal(this)"><br> </fieldset> </form> <script type="text/javascript"> var getVal = function(el){ var els = el.name ? el.ownerDocument.getElementsByName(el.name) : [el]; for(var i=0,ri = 0,re = [],el;el = els[i++];){ re[ri++] = el.checked } alert(re.join("-")) } </script>
对于类型为select-multiple的下拉框,我们也使用这种取值法,其他直接取value值就行了。下面是我的getVal函数:
var getVal = function( el ) { var type = el.type, val = el.value, prop, array; if ( type === "select-multiple") { array = el.options, prop = "selected"; } else if (type === "radio" || type === "checkbox") { array = el.name ? el.ownerDocument.getElementsByName(el.name) : [el]; } else if ( type === "select-one" ) { val = elem.selectedIndex; } if (array) {//如果不是select元素就把prop改为checked prop || (prop = "checked"); // prop is "selected" or "checked" for(var i=0,ri = 0,re = [],elem;elem = array[i++];){ re[ri++] = elem[prop]; } val = re.join("-"); } return val; }
但在什么时候调用它呢。我们必须在使用伪onchange事件前取得一次值,把它保存起来,当使用onchange事件之时,再取一次,比较是否已发生变化,如果变化就执行回调函数,然后再保存新值。由于不同的元素onchange事件也有所不同,我们采取如下方式进行。
el.attachEvent( "onbeforeactivate" , function(){ var el = window.event.srcElement, type = el.type; if(/select/.test(type)){//下拉框的数据修正在onbeforeactive事件中只会执行一次 if(el["_change_data"] === undefined) el["_change_data"] = getVal(el) }else{//其他表单元素则一直使用它进行数据修正 el["_change_data"] = getVal(el) } });
数据修正是我自造的一个词,就是把表示表单元素的状态字段放到元素的一个自定义属性上,每次我们点击表单元素都把它取出来,与最新的值相比较。毫无疑问,想触发onchange事件,点击或输入等操作是必不可须。文本域,文本区的onchange事件是在失去焦点时触发的,而像下拉框,单选框,复选框则非常实时,一点击就触发,但下拉框的数据修正非常麻烦。像其他表单元素,肯定有个失去焦点的情况,但下拉框由于是一个元素集合,它是由select标签与option标签组成的(还可能有optgroup),我们通过e.scrElement得到事件源对象永远是select标签,在option之间点击,我们无法触发失去焦点的事件。注意,由于blur不会冒泡,在这里我们使用IE特有的focusout事件。因此对于文本域,文本区,上传域等表单元素,我们使用点击事件进行模拟,数据修正在onbeforeactive事件中进行。
el.attachEvent("onfocusout" , function(){ testChange(focusoutChangeOne) });
testChange函数与jQuery非常不同。jQuery在此还使用了事件分派。我的实现没有这么绕,直接分用事件处理函数,循环执行所有回调函数。
var rselect = /select/, focusoutChangeOne = dom.oneObject(["text","password","textarea","file"]), clickChangeOne = dom.oneObject(["radio","checkbox","select-multiple","select-one"]), testChange = function (oneObject) { var e = dom.event.fix(window.event), el = e.target, type = el.type; e.live = true; if(oneObject[type] && !el.readOnly){ var data = dom.store( el, "_change_data" ),val = getVal(el); if (data === undefined || val === data ) { return; } if ( data != null || val ) { if(rselect.test(type)) dom.store(el,"_change_data",val) return dom.event.handle.call(el,e) } } }
下面是我的事件系统,经典的DE大神架构……
dom.event = { add:function(){}, remove:function(){}, handle:function(){}, fix:function(){}, fire:function(){}, analog:{} }
由于涉及到缓存系统,就无法演示了。不过在testChange 函数中,它还负责对下拉框的数据修正。说到onfocusout,IE中有经典的bug,就是单选按钮的onchange事件是由于失去焦点事件触发的,而不是用点击事件。
我与jQuery的事件系统也正是用onclick来模拟它。表单元素中像单选按钮,复选框,下拉框则在点击时就触发,因此它们用onclick模拟最合适。
el.attachEvent("onclick", function(){ testChange(clickChangeOne) });
嘛,难点已经厘清,有能力的人可以自己动手试试。
liveSetup:[function(obj){ obj.attachEvent( "onbeforeactivate" , function(){ var el = window.event.srcElement, type = el.type; if(rselect.test(type)){//数据修正 if(dom.store(el,"_change_data") === undefined) dom.store(el,"_change_data",getVal(el)) }else{ dom.store(el,"_change_data",getVal(el)) } }); },function(obj){//对text textarea file password obj.attachEvent("onfocusout" , function(){ testChange(focusoutChangeOne)//数据修正 }); },function(obj){//select checkbox radio obj.attachEvent("onclick", function(){ testChange(clickChangeOne)//事件调用与数据修正 }); }]