对于 Angular 在使用 input 事件输入中文时不会立即更新值的一些调查

最近开发过程中遇到一个问题:当我使用 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.

但这么久远的结论可能并不适用于现在的情况,因此在空闲之余探了一下究竟。

输入框监听事件

  1. keydown: 当用户按下任何键时首先触发的事件,并且该事件发生在在浏览器处理该键之前。如果一直按住某个(任意)键,会一直重复触发事件。
  2. keypress: 当按下产生字符的键时触发,发生在 keydown 之后,浏览器处理该字符之前。如果一直按住某个(能产生字符)键,会一直重复触发事件,但当输入中文(compositionstart)时不会触发。
  3. input: 当一个 <input>, <select>, 或 <textarea>元素的 value被修改(字符被实际添加到控件)时触发,因此当用户按下一个字符键,但默认阻止 keydown 或 keypress 时,该事件不会触发。如果一直按住某个(能产生字符)键,会一直重复触发事件,当输入中文(compositionstart)时仍触发。
  4. keyup: 最后一个触发的事件,当任何键被释放时触发,并且浏览器会处理该键。

Chrome 和 Firefox 之间的区别

仅针对以上提及的四个事件做了对比,并且重点对比了输入中文时的表现。由于输入中文时,输入法编辑器(IME)开始新的输入合成时(就是你在打字但是还没按空格或者 1,2,3 选择具体的字的时候)会触发 compositionstart 事件,因此也添加了compositionstartcompositionsend 事件的监听,代码如下:

<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);
}

输入数字或字母时,在两个浏览器中这四个事件均能触发,并且能正常打印出输入的值,keydownkeypress 返回的值为上一次的值,在 input 事件之后,浏览器对新值进行了处理,所以inputkeyup 都会返回最新的值,同样此时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()" />

参考链接

  1. https://bugs.jqueryui.com/ticket/5933
  2. https://www.quirksmode.org/dom/events/keys.html
  3. https://www.mutuallyhuman.com/blog/keydown-is-the-only-keyboard-event-we-need/
  4. https://javascript.info/keyboard-events
  5. https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/input_event
  6. https://developer.mozilla.org/zh-CN/docs/Web/API/Element/compositionstart_event
  7. https://angular.cn/api/forms/NgModel

Leave a Reply

Your email address will not be published. Required fields are marked *