Meituan-Dianping / mpvue

基于 Vue.js 的小程序开发框架,从底层支持 Vue.js 语法和构建工具体系。
http://mpvue.com
MIT License
20.42k stars 2.07k forks source link

对于设置了唯一key的组件, 更新数据后会把旧数据重新更新一遍 #1642

Open hereisfun opened 5 years ago

hereisfun commented 5 years ago

[扼要问题描述]

mpvue 版本号:

[mpvue@2.0.5]

最小化复现代码:

[建议提供最小化可运行的代码:附件或文本代码]

index.vue

<template>
  <view @click="add" key='container'>
    <t-text :content="'t-text count: ' + count" :key="count" />
    <text>\n real count: {{count}}</text>
  </view>
</template>

<script>
import TText from "./t-text"
export default {
  components: {
    TText,
  },
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    add() {
      this.count++
    },
  }
}
</script>

t-text.vue:

<template>
  <text>{{content}}</text>
</template>

<script>
export default {
  props: ['content']
}
</script>

问题复现步骤:

  1. 点击view

观察到的表现:

  1. 第一行(组件)的数字先从0变成1,再瞬间变回0
  2. 第二行(原生text)的数据从0变成1
hereisfun commented 5 years ago

经排查,推测是mpvue源码的问题

下面的代码,表示patch的时候会调用一遍小程序的setData

function patch () {
  corePatch.apply(this, arguments);
  this.$updateDataToMP();
}

// install platform patch function
Vue$3.prototype.__patch__ = patch;

而vue在销毁组件vnode时,也是调用patch来销毁的:

// call the last hook...
vm._isDestroyed = true;
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null);
// fire destroyed hook
callHook(vm, 'destroyed');

也就是说,如果触发了组件的销毁,而原本该位置的组件节点仍然存在时,会错误的调用一次$updateDataToMP, 将旧数据塞回去。

举例说明:

页面中有A, B, 更新后变成只有C(A, B, C都是同一个组件,key值唯一) A, B -> C 此时因为key不同,sameVnode的判定为false,在updateChildren时会首先生成C, 然后destroy A, B 也就是: create C: this.setData({$root.0_0_0: C}) destroy A和B: this.setData({$root.0_0_0: A, $root.0_0_1: B}) 因为0_0_0位置上的元素还在,所以旧数据就被重新更新上去了。

规避方法:

  1. 免改源码方法: 不使用唯一key。你可以不用key或者用索引作为key。这样updateChildren时就会走patchVnode分支,不用destroy组件节点。 尽管vue官方推荐使用唯一key,但不使用key时,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法来更新节点。另外,因为小程序没有DOM操作,节点的更新本质也是通过setData来做的,所以不用key应该也不会带来太多性能问题

  2. 改源码: 思路就是在destroy的时候不要调用updateDataToMP, 观察发现被销毁的vm会被标记为vm._isDestroyed = true, 因此在updateDataToMP中加上条件判断即可:

    function updateDataToMP () {
    var page = getPage(this);
    if (!page) {
    return
    }
    
    // 对于被销毁的节点,不更新data
    if (this._isDestroyed) {
    return
    }
    
    var data = formatVmData(this);
    diffData(this, data);
    throttleSetData(page.setData.bind(page), data);
    }

    该修改已提PR: https://github.com/Meituan-Dianping/mpvue/pull/1643