表单、表格字段,输入完毕后(还没失去焦点)立即点击【保存】按钮,同时触发onChange和onClick事件,导致保存的是onChange之前的数据
问题背景
输入表格上的 数量字段后,此时还没有失焦(如果失焦则会触发onChange事件,通过公式 数量 * 单价 = 金额,自动计算出金额),立即点击【保存】按钮,此时按理说保存接口入参应该是重新计算后的金额,但是现实是数量是更新后的金额,但是金额却是计算前的错误的金额;
问题分析
表单、表格字段,输入完毕后(还没失去焦点)立即点击【保存】按钮,同时触发onChange和onClick事件,导致保存的是onChange之前的数据。
- JavaScript 是单线程的,看似同时触发,其实不一定 “同时执行”,可能出现 click(保存)在 change 之前或过程中执行 的情况,导致数据未更新就被保存。
change 事件的触发时机:通常在输入框失焦(blur)或值改变且失去焦点时触发(如文本输入框在 blur 时触发,下拉框选择后立即触发)。
点击保存按钮时,输入框会先失焦(触发 blur),进而触发 change 事件;同时按钮的 click 事件也会触发。
若 change 事件包含异步操作(如联动计算、接口请求),click 事件(保存)可能在异步操作完成前就执行,导致数据未更新。
- 输入完毕后立即点击【保存】会触发四个事件: onchange、onblur、onfocus、onclick,浏览器中,这些事件的触发遵循 “焦点变化优先于点击” 的原则,具体顺序如下
onfocus(输入框获取焦点时已触发) → onblur(输入框失去焦点) → onchange(值改变且失焦时) → onfocus(按钮获取焦点,可选) → onclick(按钮点击)。
(特殊:select 的 onchange 在选择后立即触发,早于 onblur)
- onchange 包含异步操作,onclick(保存)中的同步代码,先执行,导致组织保存数据是未更新的数据,然后执行onChange,最后调用保存接口;
问题解决
1、场景简单的,直接使用异步任务队列即可,把onClick中的代码放入setTimeout中,这样,先走onChange的异步任务,再走onClick的异步任务;
2、场景复杂的,比如onChange时调用了接口等复杂操作,则需要结合使用状态锁;
// 以 Vue 为例
data() {
return {
isChanging: false, // 标记 onchange 是否在执行(含异步)
};
},
methods: {
// 处理 onchange 事件(含异步)
async handleChange() {
this.isChanging = true;
try {
await this.asyncOperation(); // 异步联动计算等
} finally {
this.isChanging = false; // 无论成功失败,释放状态
}
},
// 处理 onclick 保存事件
async handleSave() {
// 等待 onchange 执行完毕
if (this.isChanging) {
await new Promise(resolve => {
const check = setInterval(() => {
if (!this.isChanging) {
clearInterval(check);
resolve();
}
}, 50);
});
}
// 执行保存
await this.saveData();
}
}