Skip to content

Vue2 data 选项存在的问题

来自 Vue 的警告

javascript
new Vue({
  data: {},
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

上面的例子中,在template选项里使用了n,但在data选项中并没有初始化n,这时vue会在控制台发出警告:

[Vue warn]: Property or method "n" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.

这是因为vue2是使用的Object.defineProperty来实现数据响应式:

javascript
Object.defineProperty(data, 'xxx', {
  ...
})

vue会遍历data选项内所有的属性并进行代理,前提是要有明确的key,即需要事先声明好key才能进行遍历。在模板中使用了没有事先声明的属性,vue无法对其进行代理并做出响应。

所以改为以下写法可以消除警告:

javascript
new Vue({
  data: {
    n: null
  },
  template: `
    <div>{{n}}</div>
  `
}).$mount("#app");

Vue2 只会检查 data 内的第一层属性

但是实际开发过程中,遇到更多的是以下这种情况:

javascript
new Vue({
  data: {
    user: {
      name: 'xxx'  // user.name 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{user.name}}
      {{user.age}}
      <button @click="setAge">set age</button>
    </div>
  `,
  methods: {
    setAge() {
      this.user.age = 18
    }
  }
}).$mount("#app");

上面的实例在template中使用了未声明的属性user.age,但是此时vue并不会发出警告,因为vue只会去检查data中的第一层属性。

并且点击按钮调用setAge方法,页面中依然不会显示user.age,因为事先并没有在user中声明agevue就不会代理user.age,所以即使改变了user.age的值vue也不知道,自然也不会触发render更新视图。

Vue.set

解决上述问题有两种办法:

  • 第一种就是提前声明好所有需要的或可能产生的key,但问题是很多时候我们并不能保证后续不再继续添加属性。
  • 第二种就是使用vue提供的Vue.set/this.$set方法。
javascript
new Vue({
  ...
  methods: {
    setAge() {
      this.$set(this.user, 'age', 18)
    }
  }
}).$mount("#app");

Vue.set至少会做三件事情:

  1. 在目标对象里添加一个新的keyvalue
  2. 对新的key进行代理和监听使其同样具有响应性;
  3. 触发一次render来更新视图。

后面再需要修改这个key的值,就不需要使用Vue.set了,正常修改即可触发响应。

Vue.set 作用于数组的问题

因为数组其实也是一种对象,数组的索引index相当于key,数组的每一项相当于value。也就是说,数组也无法通过直接新增索引(新增key)来让vue做出响应。

同样的,数组也无法在一开始就确定长度且不会改变,更无法提前声明好所有的索引。所以按理来说,数组也可以使用Vue.set来定义新的元素。

但是当Vue.set作用于数组时会有一个坑,那就是vue并不会对新增的元素进行代理和监听,它只会做两件事情:

  1. 在目标数组里添加一个新的元素(索引indexvalue);
  2. 触发一次render来更新视图。
javascript
new Vue({
  data: {
    arr: ["a", "b", "c"]
  },
  template: `
    <div>
      {{arr}}
      <button @click="setD">set d</button>
      <button @click="changeD">change d</button>
    </div>
  `,
  methods: {
    setD() {
      this.$set(this.arr, '3', 'd')
    },
    changeD() {
      this.arr[3] = 'ddd'
    }
  }
}).$mount("#app");

先点击第一个按钮调用setD,可以正常显示出[ "a", "b", "c", "d" ]

然后点击第二个按钮调用changeD,这次就会发现没有任何反应,因为它根本就不具有响应性

Vue 对数组方法的篡改

实际上,大多数情况下我们不会直接操作数组的索引,更好的办法是调用数组的方法来操作数组。

所以vue基于这一点,对数组的七个常用方法进行了篡改(变更方法):

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

vue中调用数组的以上方法时,同样也会触发视图更新,并对新增的的元素进行代理和监听vue大概做了三件事情:

  1. 调用JS对应的原生方法操作数组;
  2. 对新的元素进行代理和监听使其同样具有响应性;
  3. 触发一次render来更新视图。