哈喽,前端的小伙伴们!在聊今天的 IE 兼容之前,还是先跟我一起问候下(日了)ie 的所有版本吧!

在现代浏览器中,对表单元素的输入监听一般是通过监听”input”事件来实现,但坑爹的是 ie8 及之前的版本是不支持这个事件的,基本会使用它的替代品——“propertychange”来模拟这个事件,但模拟总归是模拟,如下是我总结的它们之间的最大区别

  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 事件冒泡

这个需要是否有点强 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 设置值之前都得先设置那个全局变量

好吧,我就不喷了,直接上我的解决办法,通过使用大家很少会用到的 Object.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!阿门。

好消息!

好消息!现在你只需要使用 fixJsForIE8 即可!引入它,然后:

1
2
3
4
// 和现代浏览器的 input 事件保持一致
input.addEventListener('input', function(e) {
console.log(e)
})

 评论