Vue3 中 ref 和 v-for 的搭配使用

233 阅读4分钟

案情简介

在业务开发中遇到了一个订单提交的场景, 具体要求如下

  • 用户在创建一个订单时需要输入商品的物流信息, 该信息最少有一条, 可能有多条.
  • 用户可以添加和删除物流信息.
  • 提交时需要校验每个物流信息.
  • 订单提交后如果审核不通过仍可以修改, 添加和删除物流信息.

这块功能是之前版本中已经有的, 我需要在该功能基础上进行一次迭代开发. 然后在测试时就出bug了. 由于开发该代码的员工已离职, 导致该bug排查了很久.

bug的具体描述如下:

  • 订单提交时需要校验每个物流信息的字段是否按要求填充. 创建订单时校验功能正常, 但在修改订单时出现不校验在修改订单时新增的物流信息的问题.

在两天的debug后发现该问题是由于物流信息输入组件ref的绑定问题导致的. 以下是涉事代码

犯案人验身

  <div class="shareholderInfo-list">
    <!-- 单个物流信息填写表单 -->
    <div
      class="shareholderInfo-item"
      v-for="(logistics, index) in logisticsListInfo"
      :key="index"
    >
      <yk-form
        :ref="logisticsListFormRule[index]"
        style="max-width: 1040px"
        size="large"
        :inline="true"
        :formItems="logisticsListFormItems"
        :formParams="logistics"
      >
        ......
      </yk-form>
      <p
        v-if="index > 0"
        @click="removeLogisticsList(index)"
        class="remove-shareholderInfo"
      >
        <el-icon><Remove /></el-icon>删除
      </p>
    </div>
    <p class="add-shareholderInfo" @click="addLogisticsList">
      <el-icon><CirclePlusFilled /></el-icon>添加信息
    </p>
  </div>

涉及的代码功能可分为三大部分:

  1. 用于填写的物流信息的yk-form组件
  2. 一个添加新的物流信息的按钮
  3. 一个删除物流信息的按钮

乍一看该部分代码还算正常, 而且是线上的代码. 因此其最开始逃脱了被删除的制裁. 但之后debug时一系列的证据都将矛头指向了他. 经调查小组的连夜勘查, 最终确认其犯罪事实无误, 本着依法code的原则, 现呈列物证. 以使嫌疑人心服口服.

  • 在提交修改的订单信息时, 同样会进行一次校验. 但在校验函数中打印信息最终只获得了第一个物流信息的数据, 其他物流信息完全没有校验. 毫无疑问这是ref的绑定出了问题.

在获得这份决定性的证据后调查小组立即对和ref绑定的logisticsListFormRule展开了调查. 同时也对其相关回调removeLogisticsListaddLogisticsList进行了上门走访. 在经过了一个小时的实地调查后确认了这是一起团伙作案. 为避免后人重蹈覆辙, 现公布其犯罪细节.

团队成员介绍

嫌疑人1遗照

let logisticsListFormRule: any = reactive({});
logisticsListFormRule = ref<Array<Ref>>([]);
logisticsListFormRule.value.push(ref("logisticsListFormRule0"));

可以看到, 由于保存物流信息的组件是通过v-for动态渲染的, 因此不能使用常规的字符串ref来进行绑定. 嫌疑人1借助自己数组的身份取得了yk-form组件的信任, 负责其ref绑定. 下文使用ref[]来称呼嫌疑人1.

嫌疑人2遗照

const addGoodsListInfo = () => {
  goodsListInfo.value.push({ amount:'', goodsName: "", number:'', price:'',unit: "",});
  goodsListFormRule.value.push(ref(`goodsListFormRule${goodsListInfo.value.length - 1}`));
};

嫌疑人2和嫌疑人1配合, 当新增一个物流信息时, 就向ref[]中添加一个字符串代表新增的yk-form绑定. 下文使用 addref 称呼嫌疑人2.

嫌疑人3遗照

const removeLogisticsList = (index: number) => {
  logisticsListInfo.value.splice(index, 1);
  logisticsListFormRule.value.splice(index,1);
};

addref类似, 嫌疑人3负责在删除一个物流信息时从ref[]中删除和该信息绑定的字符串. 下文使用 delref 称呼嫌疑人3.

可以看到, 虽然使用一个字符串数组来绑定ref确实有点low, 但是其使用index来确保每个物流信息都能和一个唯一的字符串对应.逻辑还算严谨, 那么问题出在哪了呢?

问题在于ref[]的初始化. ref[]的初始化是直接添加一个字符串"logisticsListFormRule0"来和第一个物流信息填写组件进行绑定. 然后在添加新的物流信息时触发addref来添加对应的refref[]中, 删除物流信息时通过delrefref[]中删除对应的ref.

如果订单只有提交功能的话以上逻辑是可以正常工作的. 但问题在于, 订单的信息是可以被修改的. 当已有2个及以上的物流信息时, ref[]的初始化仍然只添加了第一个物流信息的绑定ref. 直接提交时只校验第一个物流信息. 如果添加一个新的物流信息的话, 会发现坐标和ref的长度对不上.

// 当是修改订单时, index是从logisticsListInfo中取值
// logisticsListFormRule的长度初始只有1, 
// 当logisticsListInfo的长度, 即物流信息多于一个时, 就会出现index超过了logisticsListFormRule的长度
// 导致绑定的ref为undefined
    <div
      class="shareholderInfo-item"
      v-for="(logistics, index) in logisticsListInfo"
      :key="index"
    >
      <yk-form
        :ref="logisticsListFormRule[index]"
        style="max-width: 1040px"
        size="large"
        :inline="true"
        :formItems="logisticsListFormItems"
        :formParams="logistics"
      >
      ...

好的, 至此案情已梳理完毕. 接下来就要看一下如果修复这个bug了. 其实也比较简单, 这个问题主要是因为初始化导致的, 可以在onMounted时根据物流信息的长度为ref[]添加字符串即可. 当然其实在面对需要给v-for生成的元素添加ref时, 可以通过使用回调函数添加的方式更加的合理.