composition event 🤔
记录于 07月21日 · 火曜日4 min read

中文输入法(包括 CJK 输入法)都有的问题:如果<input>上监听input 事件,在 Win/Mac 上每次按键都会触发。例如用拼音输入法输入动漫驿站,用户可能按键 d.o.n.g.m.a…会触发 13 次事件,但其实只需要触发一次处理输入后的结果。早年间有很多 trick 的做法,比如 keyup 判断输入框内容是否是中文。现在是可以放心使用composition 事件了。

场景是:使用Ant Design 的 Select 搜索框,监听 composition,输入结束后再搜索。

因为只是输入的判断,使用 state/hook 反而复杂了,用一个全局变量标记是否正在输入,compositionstart 为 true,compositionend 赋值 false,在获取数据前检查这个标记,是 false 才获取,主要代码如下:

window.isInputing = false; const Form = (props) => { const compositionListener = (e) => { window.isInputing = e.type === 'compositionstart'; }; const fetchData = (v) => { if (window.isInputing === false) { fetch(); } }; return ( <Select showSearch value={v} onSearch={v => fetchData(v)} /> ); }
JavaScript

如果是普通<input>可以直接addEventListener,但这里的<Select>是组件,而且没有onChange/onInput。从onFocus也获取不到实际的元素,它是一个setTimeout。所以按照文档描述现成的只能用onInputKeyDown了,修改<Select>

return ( <Select showSearch value={v} onSearch={v => fetchData(v)} onInputKeyDown={(e) => { const el = e.nativeEvent.target; el.addEventListener('compositionstart', compositionListener); el.addEventListener('compositionend', compositionListener); }} /> ); }
JavaScript

这么写就冒出几种可能性:

  • 如果按照之前的写法,compositionListener函数在组件内,浏览器认为是不同的事件,会被重复绑定。要手动添加一个标记,比如修改元素的dataset,避免重复绑定。
  • 把compositionListener函数放在组件外,只会被绑定一次,但要调用组件内部的函数——比如fetchData——就变复杂了。
  • 绑定Composition事件后,在Chrome浏览器也是先触发onChange,才到compositionend。也就是在改变全局变量标记前已经执行了onSearch。

暂时不考虑把fetchData写成hook的方案,只在现有代码基础上改动,一时想不出最优写法,先改成了如果是Chrome浏览器执行一次fetchData。

const compositionListener = (e) => { window.isInputing = e.type === 'compositionstart'; if (window.chrome) { fetchData(e.target.value); } };
JavaScript

总结来看相对麻烦的还是CompositionEvent,都2020年了问题仍然存在。以前用setTimeout延迟100ms左右,等compositionend改变了全局变量的值再做后续处理。