
本文详解为何otp输入框的focus/blur逻辑在change事件中失效,并提供基于input或keyup事件的可靠解决方案,确保用户输入单个数字后立即自动跳转至下一个输入框。
在构建六位OTP(One-Time Password)输入组件时,一个常见需求是:用户在某个输入框中输入一位数字后,焦点自动移至下一个输入框。许多开发者会自然选择 change 事件来监听值的变化并触发 focus(),但实际运行中却常发现跳转失败——控制台日志显示 blur 和 focus 被调用,但光标并未真正落到下一个输入框上。
根本原因在于 change 事件的触发时机:它仅在输入框失去焦点(blur)且值发生变更后才触发,而非实时响应输入。这意味着当用户刚键入一个数字(如“5”),此时输入框仍处于聚焦状态,change 事件尚未触发;只有当用户手动点击其他区域、按 Tab 键或主动失焦后,change 才执行——这完全违背了“即时跳转”的交互预期。
✅ 正确做法是改用 input 事件(推荐)或 keyup 事件:
- input 事件在 值每次变化时立即触发(包括输入、粘贴、删除等),语义准确且兼容性好(IE9+);
- keyup 事件则在按键释放时触发,适合需区分按键行为的场景(如排除 Ctrl+V 粘贴干扰),但需额外判断键值(如 e.key.length === 1)。
以下是优化后的核心代码(已修复ID重复、maxlength拼写、焦点竞争等问题):
const OTP_LENGTH = 6;
const textInputs = [];
const otpContainer = document.getElementById('otp-container');
for (let i = 0; i < OTP_LENGTH; i++) {
const input = document.createElement('input');
input.type = 'text'; // 使用 text 更稳妥(number 类型在部分设备上会触发软键盘数字模式但限制多)
input.maxLength = 1; // ✅ 正确属性名:maxLength(非 max-length)
input.id = `otp-input-${i}`; // ✅ 避免重复 ID(原代码所有 input id='otp-input')
input.dataset.otpPos = i;
input.className = 'otp-input';
input.addEventListener('input', (e) => {
const target = e.target;
const position = parseInt(target.dataset.otpPos, 10);
// 清空非法字符(如字母、符号),只保留数字
target.value = target.value.replace(/[^0-9]/g, '');
if (target.value && position < OTP_LENGTH - 1) {
// 延迟聚焦以避免浏览器焦点调度冲突(关键!)
setTimeout(() => {
const nextInput = textInputs[position + 1];
if (nextInput) {
nextInput.focus();
}
}, 0);
}
});
input.addEventListener('keydown', (e) => {
// 支持 Backspace 删除时反向跳转(可选增强)
if (e.key === 'Backspace' && !e.target.value && e.target.dataset.otpPos > 0) {
setTimeout(() => {
const prevInput = textInputs[parseInt(e.target.dataset.otpPos, 10) - 1];
if (prevInput) prevInput.focus();
}, 0);
}
});
textInputs.push(input);
otpContainer.appendChild(input);
}
// 自动聚焦第一个输入框
if (textInputs[0]) textInputs[0].focus();⚠️ 关键注意事项:
- 避免 input.onchange = ... 写法:使用 addEventListener 更规范,且便于解绑;
- 勿用 target.blur() 主动失焦:在 input 事件中调用 blur() 可能打断浏览器默认行为,导致后续 focus() 失效;
- setTimeout(..., 0) 是关键:将 focus() 推入宏任务队列,确保当前输入事件完成后再执行,规避浏览器焦点调度竞争;
- 移动端适配:type="text" + inputmode="numeric" 可在支持设备上唤起数字键盘,比 type="number" 更可控;
- 无障碍友好:为每个输入框添加 aria-label="OTP digit 1 of 6" 提升可访问性。
总结:OTP自动跳转的核心在于选择实时响应的事件源(input > keyup ≫ change),配合异步聚焦与输入校验,即可实现流畅、健壮、跨端一致的用户体验。










