背景
目前业界主流的 React UI 组件库中,Input 组件都没有实现是否感知输入法功能,但是这个功能在某些情况下是很有必要的,比如搜索框根据输入关键词实时搜索时,React 提供的合成事件 onChange 默认是感知输入法的,在输入过程中会产生很多【无效的输入值】(下文统一用【值】指代【输入值】)被提交到后台查询(参考下图),这些查询是没有意义的。合理情况应该是等输入完成时(即非输入法状态时),再把值提交到后台查询。
方案
首先浏览器为 IME 元素提供了 CompositionEvent 来感知输入法输入过程,其提供了 compositionstart/compositionupdate/compositionend 三个事件,我们可以借助它们来控制何时把有效值传出去。
其次在 React 提供的合成事件中,onChange 一般用在受控组件中,它是感知输入法的,为了兼容处理,我们采用另外一个合成事件 onInput(从语义上来说也更贴切),并增加一个 props(composition) 用来设置当前是否要感知输入法,这是对 onInput 功能的增强,没有破坏它的语义。
于是我们可以采用 composition/onInput 2 个 props,并借助 compositionstart/compositionend 来作开关,根据设置是否感知输入法(默认为 false,不感知)来实时把对应的值传出去。
实现
首先我们需要调研 CompositionEvent 和 InputEvent 事件的调用时序,具体可以参考 demo。这里直接贴上第三方结论.
1 | // chrome: input(emit) -> ... -> compositionstart -> compositionupdate -> input(not emit) -> compositionend(emit) |
所以我们可以在 compositionstart 时打开开关,compositionend 时关掉开关并调用 onInput 回调传值。原生 input 事件回调中,通过开关来判断是否非输入法状态(即输入完成),从而决定是否调用 onInput 回调传值。
其次是【开关】怎么设置问题。在 Class Component 中可以通过实例属性来设置,但在 Function Component 中行不通。通过 state 来控制也是不行的,因为 setState 是异步的,这会导致 input 事件回调执行时开关还没打开,从而把值传出去了。后来参考 vue 里实现可以把开关放置在 DOM 元素上(event.target)。
最后设计 Input 组件结构,大致如下:
| 属性 | 类型 | 默认值 | 备注 |
|---|---|---|---|
| defaultValue | string | 非受控默认值 | |
| value | String | 受控值 | |
| composition | boolean | false | 是否感知输入法 |
| onChange | Function | ||
| onInput | Function | 输入值,不感知输入法时只在非输入法状态调用 |
最终实现点击这里。一个简单的可感知输入法 Input 组件实现完成。
注意:因为 vue 内部对 v-model 的封装,导致 vue 组件中可能存在 value 值对不上问题。详情可参考链接。
延伸阅读
之前没注意,这次看 MDN 时才发现原生 input 和 change 事件触发时机不一样(具体参考 input event 和 change event)。总结就是 input 是在每次输入值变化时就触发,change 是在 lose focus 或是 commit value 后才触发。
[参考资料]:
