Taro受控组件的伪bug
问题
公司的taro项目有一个需求,对输入框进行金额格式限制,即用户只能输入0-9的数字以及小数点,我一看这多简单,直接把Input变成受控组件,然后监听输入框的值,每次输入是符合的就setState,不符合就setState旧值不就好了吗?然而一上手我就傻眼了,这是代码:
onInput:
onInput={(e) => {
let value = checkPrice(e.detail.value);
console.log(`value:${value}`)
setCustomPrice(value);
return value;
}}
checkPrice:
function checkPrice(price: string): string {
const exp = new RegExp("^[0-9]+(.[0-9]{1,2})?$");
let newValue = price;
let oldValue = price.length > 0 ? price.substring(0, price.length - 1) : "";
const res = exp.test(newValue) ? newValue : oldValue;
return res;
}
可以看到我的基本逻辑就是判断每次输入的值的最后一位是不是合法,如果不合法就删掉最后一位(把最后一位当旧值是不合理的,这里暂且不讨论这个)。
输入 12qwertyuio
,出现:
原本预期出现的是:
12
,结果却是12qetuo
,有点小意外,就算是不对,那也应该是没有过滤效果,怎么会一半过滤一半不过滤呢?
分析
仔细分析每一个步骤发现是这样子的:
- 输入1和2,没有问题,此时视图和值都是12
- 输入q,经过checkPrice的处理得到value是12,然后setState(12),可是视图没有变化,仍然是12q
- 输入w,此时OnInput接收到的值是12qw,checkPrice的处理得到value是12q,然后setState(12q),视图是12q
- 以此类推...
可以看出问题的关键所在是setState(12)后视图没有变化,Input组件没有完全受控,此时组件的状态是12,但是Input的状态是12q;等到输入w的时候,setState(12q),视图发生改变,是12q。
为什么setState(12)的时候Input没有发生变化呢?
这里就涉及到了React的机制,diff算法会过滤相同的值。setState(12)的时候前一个状态也是12,所以组件状态没有发生改变,而Input组件因为我们输入了q,所以变成了12q。
解决的办法在于视图改变时,立即同步状态,然后再对状态值进行进一步修改。可以在onInput的时候同步更新状态;然后在useLayoutEffect对状态进行限制,重新设置状态。
其实这是一个很简单的问题,只要略懂setState的原理以及认真分析就能看出问题所在,但是当时一遇到问题的时候我第一想到的是taro的受控组件可能有问题,所以我马上在taro的issue找类似的问题,没想到还找到很多和我一样疑惑的人,这更加印证了我的想法。但是最后看到开发人员的相关解答才发现是自己鲁莽了,所以我称这是Taro受控组件的伪bug
。
这告诉我们写代码遇到bug的时候还是要先冷静分析一下代码的流程。另外关于React setState的原理和diff的过程我也会在之后通过源码的方式进行分析。
另外附上在这个业务中我最后采用的代码:
const [customPrice, setCustomPrice] = useState<number | string>('自定义');
// ...
<Input
type='digit'
style={{ fontWeight: isCustom ? 'bold' : 'normal' }}
value={customPrice.toString()}
onInput={(e) => setCustomPrice(e.detail.value)}
placeholder='自定义'
placeholderClass={styles.placeHolder}
onFocus={onFocus}
/>
// ...
useLayoutEffect(() => {
const originalPrice = customPrice.toString();
const newPrice = checkPrice(originalPrice);
setCustomPrice(newPrice);
}, [customPrice]);
// 格式化价格
function checkPrice(price: string): string {
const originalPrice = price.toString();
// 替换非数字和小数点
const exp = new RegExp("[^(0-9|.)]");
let newPrice = originalPrice.replace(exp, "");
let pointCount = newPrice.split(".").length;
const firstPointIndex = newPrice.indexOf(".");
// 小数点后面有几位数
const afterPointCount =
firstPointIndex === -1 ? 0 : newPrice.length - firstPointIndex - 1;
const isPointValid = afterPointCount <= 2;
if (originalPrice == newPrice && pointCount <= 2 && isPointValid) {
return originalPrice;
}
// 去掉多余的小数点
pointCount = newPrice.split(".").length;
while (pointCount > 2) {
const index = newPrice.lastIndexOf(".");
newPrice =
newPrice.substring(0, index) +
newPrice.substring(index + 1, newPrice.length);
pointCount = newPrice.split(".").length;
}
newPrice = newPrice.substring(0, firstPointIndex + 3);
return newPrice;
}