列表循环里的 key 不生效

测试示例

uni-app测试代码:

<template>
    <ul class="list">
        <li v-for="item in list" :key="item.id">
            <span>姓名:{{ item.name }},</span>
            <span :class="{ 'gender-other': isGenderOther(item.gender) }">性别:{{ item.gender }}</span>
        </li>
    </ul>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({})
export default class Index extends Vue {
    list = [
        {
            id: 1,
            gender: 'male',
            name: '张三',
        },
        {
            id: 2,
            gender: 'female',
            name: '李四',
        },
        {
            id: 3,
            gender: 'other',
            name: '王五',
        },
    ]
    isGenderOther(gender) {
        return gender === 'other';
    }
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

编译后的小程序wxml

<view class="list _ul">
    <block wx:for="{{$root.l0}}" wx:for-item="item" wx:for-index="__i0__" wx:key="id">
        <view class="_li">
            <label class="_span">{{"姓名:"+item.$orig.name+","}}</label>
            <label class="{{['_span',(item.m0)?'gender-other':'']}}">{{"性别:"+item.$orig.gender}}</label>
        </view>
    </block>
</view>
1
2
3
4
5
6
7
8

编译后的小程序js部分代码:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  var l0 = _vm.__map(_vm.list, function(item, __i0__) {
    var m0 = _vm.isGenderOther(item.gender)
    return {
      $orig: _vm.__get_orig(item),
      m0: m0
    }
  })

  _vm.$mp.data = Object.assign(
    {},
    {
      $root: {
        l0: l0
      }
    }
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

编译后的wxmlitem对应的结构是

{
    $orig: _vm.__get_orig(item),
    m0: m0
}
1
2
3
4

而这个item里并没有id属性,因此最终会导致设置的key并没有起到作用。

可以发现,当在uni-app的模板的列表循环里调用组件上的函数时,会导致编译成小程序代码时对list进行了一层封装,其原因是微信小程序里不支持在wxml里调用组件里的方法,因此uni-app在编译成微信小程序时会对list做一层封装,并将函数调用的结果挂在封装后的对象的属性上。

解决方案

方案一

当将模板里的isGenderOther(item.gender)改为item.gender === 'other'时,编译后的小程序wxml就正常了。

<view class="list _ul">
    <block wx:for="{{list}}" wx:for-item="item" wx:for-index="__i0__" wx:key="id">
        <view class="_li">
            <label class="_span">{{"姓名:"+item.name+","}}</label>
            <label class="{{['_span',(item.gender==='other')?'gender-other':'']}}">{{"性别:"+item.gender}}</label>
        </view>
    </block>
</view>
1
2
3
4
5
6
7
8

该解决办法是避免uni-app编译时对list进行封装,此时最终的item上是有id属性的。

方案二

该示例里的使用函数的逻辑比较简单,咱们可以将isGenderOther(item.gender)改为item.gender === 'other',而针对某些复杂的情况,必须要在模板的列表循环里使用函数,又想让key生效,应该怎么办?

可以将key的取值也变成函数调用:

<template>
    <ul class="list">
        <li v-for="item in list" :key="getItemKey(item)">
            <span>姓名:{{ item.name }},</span>
            <span :class="{ 'gender-other': isGenderOther(item.gender) }">性别:{{ item.gender }}</span>
        </li>
    </ul>
</template>
1
2
3
4
5
6
7
8

编译后的小程序wxml

<view class="list _ul">
    <block wx:for="{{$root.l0}}" wx:for-item="item" wx:for-index="__i0__" wx:key="m0">
        <view class="_li">
            <label class="_span">{{"姓名:"+item.$orig.name+","}}</label>
            <label class="{{['_span',(item.m1)?'gender-other':'']}}">{{"性别:"+item.$orig.gender}}</label>
        </view>
    </block>
</view>
1
2
3
4
5
6
7
8

编译后的小程序js部分代码:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  var l0 = _vm.__map(_vm.list, function(item, __i0__) {
    var m0 = _vm.getItemKey(item)
    var m1 = _vm.isGenderOther(item.gender)
    return {
      $orig: _vm.__get_orig(item),
      m0: m0,
      m1: m1
    }
  })

  _vm.$mp.data = Object.assign(
    {},
    {
      $root: {
        l0: l0
      }
    }
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

编译后的wxmlitem对应的结构是

{
    $orig: _vm.__get_orig(item),
    m0: m0,
    m1: m1
}
1
2
3
4
5

key的取值变成函数调用之后,编译后的wxmlkey的取值变成了m0,而m0是存在在封装后的item上的,这样就符合了微信小程序里关于key的使用规则了。