前端 · 2025年 8月 18日 0

Vue踩坑:异步操作与数据引用

在前端开发中,使用VueElement UIel-upload组件实现文件上传是常见需求。然而,当需要处理批量上传时,一个看似简单的功能可能会埋下深坑。本文将复盘一个我在实践中遇到的问题:el-upload批量上传功能失效,明明网络请求和数据返回都正常,但数据却无法正确同步。


问题重现:现象与初步分析

我在一个表单中需要上传多份银行流水和用药明细。两者都使用el-upload,并开启了multiple属性以支持批量选择。

<el-upload action="" multiple :http-request="({ file }) => handleBankStatementUpload({ file }, index)">
  </el-upload>

<el-upload action="" multiple :http-request="({ file }) => handleMedicationBreakdownUpload({ file }, item)">
  </el-upload>

两个上传函数都接受一个数据对象作为参数,并在文件上传成功后,将文件信息push到该对象的子列表中。

// 以用药明细为例
handleMedicationBreakdownUpload({ file }, targetObject) {
  // ... 上传逻辑
  uploadCommonFile(formData).then(res => {
    // 成功后,将文件信息添加到目标对象的列表
    targetObject.listOfMedicationBreakdown.push({
      // ... 文件数据
    });
    // 通知父组件更新数据
    this.emitUpdate();
  });
}

银行流水功能一切正常,而用药明细却出了问题。当我一次性上传4张图片时,网络请求显示4个文件都上传成功并返回了正确的数据,但页面上只显示了第一张图片,后续的3张图片数据都“凭空消失”了。

我一度怀疑是emitUpdate()函数有问题,因为它会通知父组件更新数据,可能导致子组件重新渲染,从而中断了后续的上传操作。但当我注释掉它后,批量上传确实恢复了正常,这似乎证实了我的猜测。然而,这并不是一个真正的解决方案,因为父子组件的数据同步依然是必须的。


深入探究:问题的本质

经过反复调试和思考,我意识到问题并非出在emitUpdate()本身,而是出在 el-upload的异步并行特性Vue的响应式数据引用 之间的冲突。

  1. 异步并行上传: 当我们批量选择多个文件时,el-upload会为每个文件单独发起一个http-request请求。这些请求是 并行 的,而不是串行的。
  2. v-for的临时变量引用: 在我的代码中,handleMedicationBreakdownUpload 函数的第二个参数item是来自v-for循环生成的临时变量。它是一个指向当前数据对象的引用
  3. 致命的异步冲突:
    • el-upload同时发起4个上传请求,每个请求的handleMedicationBreakdownUpload函数都拿到了各自的item引用。
    • 第一个请求很快完成,并执行targetObject.listOfMedicationBreakdown.push(...)
    • 紧接着,this.emitUpdate()被调用,通知父组件数据已更新。Vue的响应式系统会重新渲染组件,这可能导致v-for循环中原有的item引用失效或被重新创建
    • 此时,其他3个请求仍在进行中。当它们成功完成后,会尝试操作各自的item引用。然而,这些引用指向的内存地址已经不再是正确的、可用的数据对象了。因此,数据添加操作失败,文件仿佛“消失”了。

这就像四个人同时往一个抽屉里放东西,但每放一次,抽屉的位置就会移动一下。最终,只有第一个人成功了,剩下的人都找不到原来的抽屉了。


最终解决方案:直接操作根数据结构

既然临时变量的引用不可靠,那么正确的做法就是 直接操作稳定、持久的数据结构

我修改了handleMedicationBreakdownUpload函数,不再依赖传入的item参数,而是通过索引直接定位到根数据对象,从而避免了引用失效的问题。

// 修改后的正确代码
handleMedicationBreakdownUpload({ file }, index) {
  const formData = new FormData();
  formData.append('file', file);
  return uploadCommonFile(formData).then(res => {
    // 检查重复文件
    for (let i = 0; i < this.localCostOfCareData.listOfHospitalisationRecords[index].listOfMedicationBreakdown.length; i++) {
      if (this.localCostOfCareData.listOfHospitalisationRecords[index].listOfMedicationBreakdown[i].name === res.fileName) {
        this.$message.error('禁止上传重复文件');
        return Promise.reject();
      }
    }
    // 直接通过根数据结构和索引来操作目标数组
    this.localCostOfCareData.listOfHospitalisationRecords[index].listOfMedicationBreakdown.push({
      id: Date.now(),
      url: res.url,
      name: res.fileName,
      dynamicTags: [],
      check: true
    });
    // 仅在数据正确添加后,通知父组件更新
    this.emitUpdate();
    return res;
  }).catch(error => {
    console.log(error);
    this.$message.error('上传失败!');
    return Promise.reject(error);
  });
},

总结与警示

这个问题的核心在于:在处理异步并行操作时,不要依赖循环生成的临时变量作为数据引用的目标。

  • 错误做法: v-for="(item, index)" ... :http-request="({ file }) => handleUpload(file, item)"。这种方式在异步并发场景下,item的引用可能会因响应式更新而失效。
  • 正确做法: v-for="(item, index)" ... :http-request="({ file }) => handleUpload(file, index)"。通过传递稳定的索引,然后在函数内部通过根数据结构来定位目标数组,从而确保操作的正确性和一致性。