vue2 响应式问题一例

771 阅读1分钟
<template>
  <div>
    <el-select v-model="obj.users" multiple>
      <el-option value="a" label="a"></el-option>
      <el-option value="b" label="b"></el-option>
      <el-option value="c" label="c"></el-option>
    </el-select>
    <div>选择结果{{ obj }}</div>
    <el-button @click="setUsers1">set users1</el-button>
    <el-button @click="setUsers2">set users2</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {},
    }
  },
  methods: {
    setUsers1() {
      this.obj = {}
      this.obj.users = undefined // 写法1
      // this.$set(this.obj, 'users', []) // 写法2
    },
    setUsers2() {
      this.$set(this.obj, 'users', [])
      // this.$forceUpdate() // 333
    },
  },
}
</script>

复现步骤

  1. 点击下拉选择, 几个选项
  2. 点击 set users1 重新赋值 obj, 为空对象, 再给 obj 一个 users 属性, 值为 undefined
  3. 点击 set users2 给 obj 的 users 属性设置为 空数组
  4. 再次在下拉框中选择

期望

选择结果显示出来

实际

下拉选择之后没反应, 选择结果还是空的

原因

setUsers1 中给 obj 赋值 users 属性时, 没有使用 this.$set, 导致 vue 没有给 obj 的 users 属性设置响应. 在这种情况下, 调用 setUsers2 中的 this.$set 时, vue 内部判断 obj 上已经有 users 属性, 认为已经进行了响应式监听, 就直接给这个属性赋值, 没有触发更新的广播

Vue.$set 源码

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val // 这里, 如果对象上已经有这个属性了, 赋值完直接返回, 就不会走到下面依赖广播的逻辑了
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

如何修正

  • 方法1: setUsers1 中, 写法1 改成 写法2 或直接写成 this.obj = { users: undefined }
  • 方法2: setUsers2 中打开注释 333

最佳实践

给 vue 实例的属性对象上添加属性时, 不要直接赋值, 要用 Vue.$setthis.$set