Vue 之 Data

描述

Vue 实例的数据对象。Vue 将会递归将 data 的属性转换为 getter/setter,从而让 data 的属性能够响应数据变化。对象必须是纯粹的对象 (含有零个或多个的 key/value 对):浏览器 API 创建的原生对象,原型上的属性会被忽略。大概来说,data 应该只能是数据 - 不推荐观察拥有状态行为的对象。

一旦观察过,不需要再次在数据对象上添加响应式属性。因此推荐在创建实例之前,就声明所有的根级响应式属性。

实例创建之后,可以通过 vm.$data 访问原始数据对象。Vue 实例也代理了 data 对象上所有的属性,因此访问 vm.a 等价于访问 vm.$data.a

_$ 开头的属性 不会 被 Vue 实例代理,因为它们可能和 Vue 内置的属性、API 方法冲突。你可以使用例如 vm.$data._property 的方式访问这些属性。

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

如果需要,可以通过将 vm.$data 传入 JSON.parse(JSON.stringify(...)) 得到深拷贝的原始数据对象。

注意,如果你为 data 属性使用了箭头函数,则 this 不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。

data: vm => ({ a: vm.myProp })

Vue组件中的data为什么是函数

类比引用数据类型

Object是引用数据类型,如果不用function 返回,每个组件的data 都是内存的同一个地址,一个数据改变了其他也改变了;

javascipt只有函数构成作用域(注意理解作用域,只有函数的{}构成作用域,对象的{}以及 if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。

举个例子:

const MyComponent = function( ) {};
MyComponent.prototype.data = {
  a: 1,
  b: 2,
}
const component1 = new MyComponent();
const component2 = new MyComponent();

component1.data.a === component2.data.a; // true
component1.data.b = 5;
component2.data.b // 5

如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改;

两个实例应该有自己各自的域才对,需要通过下面的方法来进行处理:

const MyComponent = function( ) {
  this.data = this.data();
};  
MyComponent.prototype.data = function( ) {
  return {
    a: 1,
    b: 2,
  }
};

这样么一个实例的data属性都是独立的,不会相互影响了。

所以,你现在知道为什么vue组件的data必须是函数了吧。这都是因为js本身的特性带来的,跟vue本身设计无关。其实vue不应该把这个方法名取为data(),应该叫setData或其他更容易理解的方法名。

不要把所有东西都放进data里

Vue组件实例中的data是我们再熟悉不过的东西了,用来存放需要绑定的数据但是对于一些特定场景,data虽然能够达到预期效果,但是会存在一些问题我们写下如下代码,建立一个名单,记录他们的名字,年龄和兴趣:

<!DOCTYPE html>
<html >
<head>
  <meta charset="UTF-8">
  <title>Data</title>
</head>

<body>
  <div > </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  <script> const template = ` <div> <h3>data列表</h3> <ol> <li v-for="item in dataList"> 姓名:{{item.name}},年龄:{{item.age}},兴趣:{{item.hobby.join('、')}} </li> </ol> </div> ` new Vue({ el: '#app', data () { return { dataList: [ { name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] }, { name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] }, { name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] }, { name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] }, { name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] }, { name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] } ], } }, mounted () { console.table(this.dataList) // 打印列表形式的dataList console.log(this.dataList) // 打印字面量 }, template }) </script>
</body>
</html>

Vue通过data生成我们能用的绑定数据,大概走了以下几个步骤:

  1. initData 方法 中获取你传入的data,校验data是否合法
  2. 调用 observe 函数,新建一个 Observer 实例,将 data 变成一个响应式对象,而且为data添加 __ob__ 属性,指向当前 Observer实例
  3. Observer 保存了你的 value 值、数据依赖 depvue 组件实例数vmCount
  4. 对你的data调用 defineReactive$$1 递归地监所有的 key/value(你在data中声明的),使你的key/value都有自己的 depgettersetter

我们忽略html的内容,重点放在这个 dataList 上(我用2种不同的形式打印了dataList),如上述步骤2、3、4所说,data中每个key/value值(包括嵌套的对象和数组)都添加了一个Observer。

之前我们说滥用data会产生一些问题,问题如下:

设想一下这样的场景,如果你的data属于纯展示的数据,你根本不需要对这个数据进行监听,特别是一些比这个例子还复杂的列表/对象,放进data中纯属浪费性能。

那怎么办才好?

放进computed中

还是刚才的代码,我们创建一个数据一样的list,丢进computed里:

computed: {
  computedList () {
    return [
      { name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] },
      { name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] },
      { name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] },
      { name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] },
      { name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] },
      { name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] }
    ]
  }
},

打印computedList,你得到了一个没有被监听的列表

为什么computed没有监听我的data

因为我们的computedList中,没有任何访问当前实例属性的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项。

我们来看官方文档对computed的说明:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function ( ) {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

这里强调的是

所以,对于任何复杂逻辑,你都应当使用计算属性。

但是很少有人注意到api说明中的这一句:

计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。

也就是说,对于纯展示的数据,使用computed会更加节约你的内存

另外 computed 其实是Watcher的实现,有空的话会更新这部分的内容

为什么说“至少2.0是如此”

因为3.0将使用Proxy来实现监听,性能将节约不少,参见https://www.jianshu.com/p/f99822cde47c