最近在开发一个商品管理系统时遇到了一个奇怪的问题:在使用 uniapp 开发的页面中,有一个商品列表,每个商品都有数量输入框,但是在输入框中输入数字时,每输入一个字符,输入框就会自动失去焦点,用户体验非常糟糕。

问题场景

具体场景是这样的:用户扫描商品二维码后,会在 productList 数组中添加一个商品对象,每个商品对象都包含商品信息和数量字段,数量字段与 input 输入框进行双向绑定。用户需要在输入框中修改商品数量,但每次输入都会导致输入框失焦。

错误代码

<view v-for="(item, i) in productList" :key="item">
  <view class="product-item">
    <view class="label-section">
      <text>商品数量</text>
      <text class="required-mark">*</text>
    </view>
    <input
      v-model="item.quantity"
      class="quantity-input"
      placeholder="请输入数量"
      type="number"
      @input="updateQuantity($event, i)"
    />
    <text class="unit-text">件</text>
  </view>
</view>
updateQuantity(e, i) {
  const value = e && e.detail.value;
  this.$nextTick(() => {
    this.productList = this.productList.map((v, index) => ({
      ...v,
      quantity: parseInt(i) === parseInt(index) ? parseInt(value) : v.quantity,
    }));
  });
},

问题原因

问题的根本原因在于 v-for 中的 :key="item" 设置不当。当 input 数据发生变化并触发模型更新后,Vue 需要重新渲染列表项。由于 key 值绑定的是整个 item 对象,而 item 对象中的 quantity 属性发生了变化,导致 Vue 认为这是一个新的列表项,从而重新创建了整个 view 组件,包括其中的 input 元素。

在 Vue 的 diff 算法中,key 是用来标识列表项唯一性的重要属性。如果 key 值发生变化,Vue 会认为这是一个全新的元素,而不是对原有元素的更新,因此会销毁旧元素并创建新元素,这就导致了 input 失去焦点。

解决方案

解决方法很简单,将 key 值改为数组索引或者对象中不会变化的唯一标识:

<view v-for="(item, i) in productList" :key="'product-' + i">
  <view class="product-item">
    <view class="label-section">
      <text>商品数量</text>
      <text class="required-mark">*</text>
    </view>
    <input
      v-model="item.quantity"
      class="quantity-input"
      placeholder="请输入数量"
      type="number"
      @input="updateQuantity($event, i)"
    />
    <text class="unit-text">件</text>
  </view>
</view>

或者如果商品对象有唯一的 ID 字段,使用 ID 作为 key 值会更好:

<view v-for="(item, i) in productList" :key="item.id">
  <!-- 其他代码保持不变 -->
</view>

总结

在使用 v-for 循环渲染列表时,key 的选择非常重要:

  1. 不要使用会变化的对象作为 key:如果对象的属性会发生变化,Vue 会认为这是不同的元素
  2. 优先使用唯一且稳定的标识:如数据库 ID、唯一编码等
  3. 谨慎使用数组索引:只有在列表不会发生增删改操作时才推荐使用索引作为 key
  4. 避免使用 random 或时间戳:这会导致每次渲染都重新创建元素

这个问题在 Vue 开发中比较常见,特别是在处理表单列表时。正确理解 Vue 的 key 机制,能够避免很多类似的渲染问题.