最近开发过程中遇到一个问题:当我使用 input 作为输入框的监听事件,并且输入中文做查询时,发现在 Chrome 中并不能在敲完回车或空格之后立即更改值,而是需要等到下一次 keydown
事件(按删除或者其他键)发生时才会获取新的值。
在经过广泛的搜索之后找到一篇 12 年前的帖子,上面有一个回答这么描述到:
https://bugs.jqueryui.com/ticket/5933
I figured out a way to actually type in Chinese characters on OS X. What I noticed is that Chrome doesn’t trigger any key events, but does trigger an input event. Firefox doesn’t trigger keydown or keyup, but does trigger keypress and input. I couldn’t get the characters to even get typed in IE through Virtual Box. So the problem is that the browsers don’t report the events and therefore I don’t think this is a problem that we can fix.
但这么久远的结论可能并不适用于现在的情况,因此在空闲之余探了一下究竟。
输入框监听事件
keydown
: 当用户按下任何键时首先触发的事件,并且该事件发生在在浏览器处理该键之前。如果一直按住某个(任意)键,会一直重复触发事件。keypress
: 当按下产生字符的键时触发,发生在 keydown 之后,浏览器处理该字符之前。如果一直按住某个(能产生字符)键,会一直重复触发事件,但当输入中文(compositionstart
)时不会触发。input
: 当一个<input>
,<select>
, 或<textarea>
元素的value
被修改(字符被实际添加到控件)时触发,因此当用户按下一个字符键,但默认阻止 keydown 或 keypress 时,该事件不会触发。如果一直按住某个(能产生字符)键,会一直重复触发事件,当输入中文(compositionstart
)时仍触发。keyup
: 最后一个触发的事件,当任何键被释放时触发,并且浏览器会处理该键。
Chrome 和 Firefox 之间的区别
仅针对以上提及的四个事件做了对比,并且重点对比了输入中文时的表现。由于输入中文时,输入法编辑器(IME)开始新的输入合成时(就是你在打字但是还没按空格或者 1,2,3 选择具体的字的时候)会触发 compositionstart
事件,因此也添加了compositionstart
和 compositionsend
事件的监听,代码如下:
<input id="testinput" [(ngModel)]="testval" (keydown)="testkeydown($event)" (keypress)="testkeypress($event)" (keyup)="testkeyup($event)" (input)="testinput($event)" />
testval: string = ''; ngOnInit(): void { document.getElementById('testinput')?.addEventListener('compositionstart', () => { console.log('compositionstart'); }); document.getElementById('testinput')?.addEventListener('compositionend', () => { console.log('compositionend'); }); } testkeydown(e:any) { console.log('keydown', this.testval); } testkeyup(e:any) { console.log('keyup', this.testval); } testkeypress(e:any) { console.log('keypress', this.testval); } testinput(e:any) { console.log('input', this.testval); }
输入数字或字母时,在两个浏览器中这四个事件均能触发,并且能正常打印出输入的值,keydown
和keypress
返回的值为上一次的值,在 input
事件之后,浏览器对新值进行了处理,所以input
和keyup
都会返回最新的值,同样此时ngModelChange
事件触发, ngModel
绑定的值也更新了。
当输入中文时,以“啊”为例,从拼音输入到按下空格的过程中,四个事件的对比结果如下:
打印的格式为【事件名 + ngModel 绑定的变量值】
Chrome
keydown compositionstart input keyup keydown input compositionend 啊 keyup 啊
Firfox
keydown <empty string> compositionstart <empty string> input <empty string> keyup <empty string> keydown <empty string> compositionend 啊 input 啊 keyup 啊
通过打印出的结果可以明显的发现,Chrome 最后的 compositionend
发生在 input
之后,而 Firefox 的 compositionend
发生在前面。
所以这里猜测是由于 Chrome 并没有在 input
事件之前先完成 compositionend
事件 report,因此只能在keyup
的时候获取到最新的输入,但是没有一个准确的验证的方式,有待进一步研究。
So, emmmmm… This is a problem that we can’t fix? 😎
缓解方法
ngModelChange
Angular 中 NgModel 是一个内置指令,根据领域对象创建一个 FormControl 实例,并把它绑定到一个表单控件元素上,更新视图模型后,ngModelChange
作为事件发射器,向外抛出值。
// 它需要 ngModel 指令存在时才能使用 <input [(ngModel)]="value" (ngModelChange)="onChange()" />
keyup
由于 keyup
事件在任意键释放时都会触发,并且浏览器已经在 input 时处理过了该键,所以这个时候的值已经是最新的了。
// 只要把 (input) 替换为 (keyup) 即可 <input [(ngModel)]="value" (keyup)="onChange()" />
参考链接
- https://bugs.jqueryui.com/ticket/5933
- https://www.quirksmode.org/dom/events/keys.html
- https://www.mutuallyhuman.com/blog/keydown-is-the-only-keyboard-event-we-need/
- https://javascript.info/keyboard-events
- https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/input_event
- https://developer.mozilla.org/zh-CN/docs/Web/API/Element/compositionstart_event
- https://angular.cn/api/forms/NgModel