前言
最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 11 位手机号的显示需要按照 3 4 4 的格式显示,比如 138 1234 5678。实现起来倒也不麻烦,写个格式化函数就可以了。但是后面自己测试的时候,发现了一个问题,就是格式化后在删除或者新增数字的时候,光标会发生奇怪的跳到。具体情况详见下面
案例展示
代码采用 react 写的,具体如下:
js
const [formmatPhone, setFormmatPhone] = useState('');
const getFormmatPhone = (phone) => {
// 按 3 4 4 的格式格式化,中间添加空格
if (phone.length <= 3) {
return phone;
} else if (phone.length <= 7) {
return `${phone.slice(0, 3)} ${phone.slice(3)}`;
} else {
return `${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`;
}
};
const handleChange = (e) => {
console.log(e.target.value);
// 只需前11位数字
const value = e.target.value.replace(/[^\d]/g, '').slice(0, 11);
setFormmatPhone(getFormmatPhone(value));
};
return <input type="tel" value={formmatPhone} onChange={handleChange} />;
具体可以看下图,主要就是在空格处进行删除或者有空格前面添加会导致光标跳动到最后。 但是假如将代码 handleChange 时 setFormmatPhone 不再进行 getFormmatPhone,直接 setFormmatPhone(value)就不会产生该问题。

问题分析
这个问题主要是由 React 的受控组件+ 格式化输入共同作用引起的。核心原因是:每次输入变更时,setFormmatPhone(getFormmatPhone(value)) 会更新 value,但这个新值和用户当前光标位置不一致,React 会强制让 input 重新渲染,从而把光标跳到结尾。 因此,主要原因就在于 判断当前 DOM input 的值是否和即将渲染的值一样
以 "138 1234 567" 为例:
- 用户在 1234 前面输入一个数字变为 "138 11234 567";
- 然后代码调用 getFormmatPhone() 自动格式化为 "138 1123 4567";
- 但此时 React 不知道你当前光标在 "1123" 的哪里,于是它默认把光标放到结尾。
解决方案
询问 ai 后,给出的解决方案是手动设置光标位置,具体代码如下:
jsx
const [formmatPhone, setFormmatPhone] = useState('');
const inputRef = useRef(null);
const getFormmatPhone = (phone) => {
// 按 3 4 4 的格式格式化,中间添加空格
if (phone.length <= 3) {
return phone;
} else if (phone.length <= 7) {
return `${phone.slice(0, 3)} ${phone.slice(3)}`;
} else {
return `${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`;
}
};
const handleChange = (e) => {
const rawValue = e.target.value;
const selectionStart = e.target.selectionStart;
// 提取数字
const numeric = rawValue.replace(/\D/g, '').slice(0, 11);
const formatted = getFormmatPhone(numeric);
// 计算新光标位置(根据数字在旧字符串中位置推断)
const digitsBeforeCursor = rawValue
.slice(0, selectionStart)
.replace(/\D/g, '').length;
let newCursor = 0;
let digitCount = 0;
for (let i = 0; i < formatted.length; i++) {
if (/\d/.test(formatted[i])) digitCount++;
if (digitCount === digitsBeforeCursor) {
newCursor = i + 1;
break;
}
}
if (digitsBeforeCursor === 0) newCursor = 0;
if (digitCount < digitsBeforeCursor) newCursor = formatted.length;
setFormmatPhone(formatted);
// 等待 DOM 更新后设置光标
requestAnimationFrame(() => {
inputRef.current?.setSelectionRange(newCursor, newCursor);
});
};
return (
<input
type="tel"
ref={inputRef}
value={formmatPhone}
onChange={handleChange}
/>
);