Safari中如何检测触发compositionend事件的对应keydown事件?
这个Safari的事件顺序坑确实让人头大!我之前做输入法相关交互的时候也踩过类似的雷,先帮你理清楚问题的核心,再给你几个可行的解决思路。
首先得明确不同浏览器的事件顺序差异,这是问题的根源:
Chrome/Firefox 中的事件顺序
当用中文输入法输入a再按4选候选词时,事件顺序是这样的:
[keydown] key: a; currently in composing mode: false [compositionstart] data: [compositionupdate] data: [keydown] key: 4; currently in composing mode: true [compositionend] data: 呵
这里触发compositionend的keydown(也就是选候选的4)发生在compositionend之前,而且isComposing是true,很容易和composition关联起来。
Safari 中的事件顺序
但Safari的逻辑完全反了,让人摸不着头脑:
[compositionstart] data: [compositionupdate] data: [keydown] key: a; currently in composing mode: true [compositionend] data: 樂 [keydown] key: 4; currently in composing mode: false
触发compositionend的那个keydown(选候选的4)居然跑到了compositionend之后,而且isComposing是false,和正常的keydown事件看起来一模一样,这就难区分了。
你担心的「把后续正常keydown误判成触发compositionend的事件」确实是个大问题——尤其是用户用鼠标点候选词的时候,根本不会触发keydown事件,这时候直接抓compositionend后的第一个keydown肯定会翻车。
可行的解决方案:状态标记+时间戳关联
核心思路是利用「系统触发的compositionend和对应keydown的时间差极小」这个特点,加上状态标记来区分“刚结束composition后的关联keydown”和“用户正常输入的keydown”。
具体实现步骤:
- 维护几个状态变量,跟踪composition的生命周期和结束时间
- 在
compositionend触发时,设置“等待关联keydown”的标记,并记录结束时间 - 用一个短定时器自动清空标记,避免误判后续的正常输入
- 在
keydown事件中,检查标记和时间差,判断是否为触发compositionend的事件
代码示例
// 维护状态变量 let inComposition = false; let lastCompositionEndTime = 0; let isWaitingForKeydownAfterComposition = false; // 时间阈值:根据实际测试调整,150ms足够区分系统事件和用户手动输入 const KEYDOWN_COMPOSITION_THRESHOLD = 150; let thresholdTimer = null; function log(message) { const out = document.getElementById('out'); out.appendChild(document.createTextNode(message)); out.appendChild(document.createTextNode('\n')); } function main() { const inp = document.getElementById('inp'); inp.addEventListener("keydown", function(evt) { const now = Date.now(); if (isWaitingForKeydownAfterComposition && (now - lastCompositionEndTime) < KEYDOWN_COMPOSITION_THRESHOLD) { // 这个keydown就是触发上一次compositionend的事件 log(`[关联成功] keydown key: ${evt.key} 触发了上一次compositionend`); // 清空标记,避免重复触发 isWaitingForKeydownAfterComposition = false; if (thresholdTimer) { clearTimeout(thresholdTimer); thresholdTimer = null; } } else { // 正常的keydown事件 log(`[keydown] key: ${evt.key}; currently in composing mode: ${evt.isComposing}`); } }); inp.addEventListener("compositionstart", function(evt) { inComposition = true; isWaitingForKeydownAfterComposition = false; if (thresholdTimer) { clearTimeout(thresholdTimer); thresholdTimer = null; } log(`[compositionstart] data: ${evt.data}`); }); inp.addEventListener("compositionupdate", function(evt) { log(`[compositionupdate] data: ${evt.data}`); }); inp.addEventListener("compositionend", function(evt) { inComposition = false; lastCompositionEndTime = Date.now(); isWaitingForKeydownAfterComposition = true; log(`[compositionend] data: ${evt.data}`); // 超时自动清空标记,避免误判后续正常输入 thresholdTimer = setTimeout(() => { isWaitingForKeydownAfterComposition = false; }, KEYDOWN_COMPOSITION_THRESHOLD); }); } window.onload = main;
补充说明
- 时间阈值调整:可以根据实际测试微调,100-200ms之间比较合适,既能覆盖Safari的事件延迟,又能避免用户快速连续输入导致的误判。
- 鼠标选候选的情况:当用户用鼠标点击候选词时,不会触发keydown事件,定时器会自动清空标记,后续的正常keydown会被正确识别,不会误判。
- 兼容性:这个方案不依赖任何私有API,在所有支持composition事件的浏览器都能正常工作,包括Safari。
备选思路:直接利用compositionend的data
如果你的场景不需要严格关联keydown和compositionend,只是想区分「输入法候选输入」和「正常键盘输入」,那其实可以直接通过compositionend的data来判断——只要compositionend有非空的data,就说明是通过输入法候选输入的内容,不管用户是用鼠标还是键盘选的,这样反而更简单。
备注:内容来源于stack exchange,提问作者shabunc

