哈喽,前端的小伙伴们!在聊今天的IE兼容之前,还是先跟我一起问候下(日了)ie的所有版本吧!
  在现代浏览器中,对表单元素的输入监听一般是通过监听”input”事件来实现,但坑爹的是ie8及之前的版本是不支持这个事件的,基本会使用它的替代品——“propertychange”来模拟这个事件,但模拟总归是模拟,如下是我总结的它们之间的最大区别

propertychange和input的区别

  1. propertychange的触发条件并不仅仅是输入框的value被改变,任何属性的改变都会触发(比如:class,attribute等)
  2. propertychange事件不会冒泡,也就是说不能够像oninput那样进行事件托管
  3. propertychange事件并不区分事件的调用来源,用户输入会触发,js改变也会触发。而”input”事件往往都只需要关注用户的输入,这就容易造成事件的误触发

ok,下面来针对上面的每一条来一一给出解决办法,实现完美模拟!(全网独家!)

一.让propertychange只关注”value”变化

这里的value是个泛指,如果监听对象是select(下拉选框),它的value就是selectedIndex

上代码:

1
2
3
4
5
input.onpropertychange=function(e){
if(e.propertyName == "value" || e.propertyName == "selectedIndex"){
//这里写事件处理即可
}
}

以上代码可以看出propertychange触发的事件对象中有属性”propertyName”,它代表的含义是本次是表单元素的哪个属性被改变了,对它进行过滤来实现模拟1

二.让propertychange事件冒泡

  这个需要是否有点强IE8所难呢?的确,该事件本身是并不支持的,那我们只能想点歪门斜道了,通过监听”focusin”来变通实现

实现思路如下:

  这种需求一般是想要进行事件托管,通过监听表单元素的父级或者document/window对象来方便托管一切表单元素,这种实现方式稳定又高效,但这一切是基于该事件能够冒泡到顶层。虽然propertychange事件不支持冒泡,但”focusin”事件是支持的。不过它俩的职责不同啊,一个是监听属性变化,一个是监听焦点变化,如何联系?

  大家想一下,如果要模拟input事件,一切的事件触发都是基于用户的输入,但输入之前必然得先让表单元素获取焦点,那是否可以这样,当输入框获取焦点的时候再绑定propertychange呢?

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
document.onfocusin=function(e){
//target即为此时获取焦点的元素
var target=e.srcElement;
//这里再保证一下此时该元素确实获取焦点(focusin事件也有坑爹的地方,暂且不表)
if(document.activeElement == target){
target.onpropertychange=function(e){
if(e.propertyName == "value" || e.propertyName == "selectedIndex"){
//这里写事件处理即可
}
}
}
}

三.让propertychange过滤js对值的改变

  这条我觉得才是重头戏!网上也有相关实现,但解决方案无非两种:

  1. 干脆就不监听propertychange,通过监听表单元素的”keydown”,”cut”,”paste”等一系列输入事件来模拟
  2. 在js设置value之前先主动告诉某变量我正在用js改变value,propertychange于是忽略。于是你在每次用js设置值之前都得先设置那个全局变量

  好吧,我就不喷了,直接上我的解决办法,通过使用大家很少会用到的api:defineProperty(点我查看用途)

  如果你已经了解了defineProperty,我估计你已经知道我接下来要干啥了。

  其实思路说出来很简单,就是如果在js改变表单元素值的时候,能自动通知我不就完事大吉了吗?而defineProperty就是干这事的!啊,不对,应该是一不小心干了这事。。

错误代码如下,错误代码如下,错误代码如下(重要的事情说三遍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//我是那个用来判断是否是js改变表单元素值的全局变量setValByJs
var setValByJs=false;
Object.defineProperty(input,"value",{
set:function(val){
this.value=val;
setValByJs=true;
},
get:function(){
return this.value;
}
})

input.onpropertychange=function(e){
if(e.propertyName == "value" || e.propertyName == "selectedIndex"){
if(setValByJs){
setValByJs=false;
}else{
//这里写事件处理即可
}
}
}

  为什么上面的代码是错的?因为会无限递归(好一个自言自语==)。由代码可以看出,在set方法的内部又调用了this.value=xx,于是就会继续再调用set,所以无限递归了。为毛非得”this.value=xx”呢,因为不这样的话,项目中的如下代码就会彻底失效了:

1
input.value="我是单身狗,汪汪汪";

方法总比困难多,机智的我又想到另外一种设置value的办法

1
input.setAttribute("value","我是单身狗,汪汪汪");

ok,那再修改一下上面的代码

还是错误代码如下,错误代码如下,错误代码如下(重要的事情说三遍):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//我是那个用来判断是否是js改变表单元素值的全局变量setValByJs
var setValByJs=false;
Object.defineProperty(input,"value",{
set:function(val){
this.setAttribute("value",val);
setValByJs=true;
},
get:function(){
return this.getAttribute("value");
}
})

input.onpropertychange=function(e){
if(e.propertyName == "value" || e.propertyName == "selectedIndex"){
if(setValByJs){
setValByJs=false;
}else{
//这里写事件处理即可
}
}
}

  那么问题来了,为毛上面的代码还错啊?!且听我仔细分析:
  当js对表单元素设置值的时候,首先会触发defineProperty中对value定义的set方法,然后代码走啊走啊,当走到:

1
this.setAttribute("value",val);

  这一行的时候,代码就会立刻跳到input.onpropertychange方法中去。。也就是说你还没来得及设置setValByJs呢,事件就被捕获了,故而对于托管方法来说,setValByJs的值是啥永远是后知后觉的。为什么我会了解这么清楚?好吧,这都是我那时候遇到的坑,出于大家坑才是真的坑的心态,故放出来大家一起坑。所以上面的代码只需要把:

1
setValByJs=true

  移到set方法的第一行即可。。

基本正确的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//我是那个用来判断是否是js改变表单元素值的全局变量setValByJs
var setValByJs=false;
Object.defineProperty(input,"value",{
set:function(val){
setValByJs=true;
this.setAttribute("value",val);
},
get:function(){
return this.getAttribute("value");
}
})

input.onpropertychange=function(e){
if(e.propertyName == "value" || e.propertyName == "selectedIndex"){
if(setValByJs){
setValByJs=false;
}else{
//这里写事件处理即可
}
}
}

  为什么又是基本正确呢?因为以上的例子基本全是事件的单一绑定,多绑定的坑还有很多。
这里提一点最容易被坑的吧,就是这段代码:

1
2
3
4
5
if(setValByJs){
setValByJs=false;
}else{
//这里写事件处理即可
}

  如果该表单元素的propertychange事件绑定了多个监听方法,只有第一个方法里会获取到setValByJs的正确值,后面的获取到的永远都是false.大家好好看下代码便知原因。这种情况也是得用点歪门斜道解决:

1
2
3
4
5
6
7
if(setValByJs){
setTimeout(function(){
setValByJs=false;
},0)
}else{
//这里写事件处理即可
}

  而且以上代码没有覆盖到所有表单元素,比如上文中提到的下拉选择框,它一般不直接监听”value”,不过核心思路都在这了,希望对你有用!希望世上再没有IE!阿门

 
其实之前有写过IE8兼容插件fixJSForIE8通过它可以让ie8兼容各种js的新特性并无须更改现有代码。只是目前该插件正在重构中,老的版本还并不能完美模拟”oninput”以及写得很烂。。着急的话可以先凑合用。主要目前缺少IE8的测试机器(不想装虚拟机)一旦更新,我会推送的。那就关注我的git吧~