Vue 数据响应式

243 阅读3分钟

当一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。

这些 getter/setter 对用户是不可见的,但它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

Vue 如何实现响应式

getter/setter

getter/setter 用于对属性的读写进行监控

let obj = {
  lastName: '张',
  firstName: '三',
  get name(){
	return this.lastName + this.firstName
  },
  set name(name){
    this.lastName = name[0],
    this.firstName = name.substring(1)
  }
}

obj.name   //'张三'

obj.name = '李四'
console.log(obj.lastName)  //李
console.log(obj.firstName)  //四

Object.defineProperty

Object.defineProperty 用于给对象添加新属性,也可以给对象添加 getter/setter

let obj = {}

Object.defineProperty(obj, 'x', {value: 1})

Object.defineProperty(obj, 'y', {
  get(){...}
  set(value){...}
})

监听与代理

let myData = {n: 0}
let newData = proxy({data: myData}) //括号里是匿名对象,无法访问

function proxy({data}){  //结构赋值
  //监听data
  let value = data.n
  Object.defineProperty(data, 'n', {
    get(){
      return value
    },
    set(newValue){
      value = newValue
    }
  })
  
//代理
	const obj = {}
  Object.defineProperty(obj, 'n', {
    get(){
      return value
    },
    set(newValue){
      value = newValue
    }
  })
  return obj  //obj 就是代理
}

vm = new Vue({data: myData}) 就做了和上面代码类似的事情。

  1. vm 成为了 myData 的代理
  2. 会对 mydata 的所有属性进行监控

目的: 无论直接修改 myData.n 还是修改 vm.nvm 都会收到通知,然后调用触发重新渲染

Vue 对 methods 和 computed 也有类似处理。

data 的对象

Vue2 通过 Object.defineProperty(obj, 'n', {...} 来实现数据的响应式,data 中必须有 'n' 才可以监听&代理 obj.n,若无 'n',就会产生问题

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

data.ndata.n 的值为 undefined ,此时 n 被引用,不会有显示,控制台会报警告

//有 data.obj,但无 data.obj.n
const vm = new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.n}}
      <button @click="setN">set n</button>
    </div>
  `,
  methods: {
    setN() {
      this.obj.n = 1;
    }
  }
}).$mount("#app");

data.obj 中开始并没有 n,后面运行 vm.obj.n = 1 并不会让 n 出现在页面中,因为 vm 没有监听&代理 data.obj.n

解决办法:

  1. 在开始就定义好 data.obj.n = undefined(声明响应式 property,推荐
  2. 使用 Vue.set 或者 this.$set
methods: {
    setN(){
        Vue.set(this.obj, 'n', 1)
        //或
        this.$set(this.obj, 'n', 1)
    }
}

一个特殊例子

//html
<div id="app">
    <span class=span-a>
      {{obj.a}} 
    </span>
    <span class=span-b>
      {{obj.b}}
    </span>
  </div>
js

//js
var app = new Vue({
  el: '#app',
  data: {
    obj: {
      a: 'a',
    }
  },
})
app.obj.a = 'a2'
app.obj.b = 'b'

最终 span-a 中会显示 a2,span-b 中显示 b。

这是因为视图更新是异步的,a1 变成 a2 时,Vue 监听到这个变化,并不会马上更新视图,而是创建一个视图更新任务到任务队列里。然后继续运行代码 app.obj.b = 'b'。视图更新时,Vue 会去做 diff,发现 a 和 b 都变了,于是去更新 span-a 和 span-b。

data 中的数组

Vue 不能检测数组的以下变动:

  1. 利用索引直接设置一个数组项
  2. 修改数组长度
let vn = new Vue({
  data: {
    array: ["a", "b", "c"]
  }
})

vm.array[1] = 'x'  // 不是响应性的
vm.array.length = 2  // 不是响应性的

以上代码并不会触发对 array 的更新。

解决方法:

  1. 如何设置数组项:

    使用 Vue.set 或者 vm.$set,可以实现 vm.array[1] = 'x' 相同的效果,也将在响应式系统内触发状态更新。

  2. 如何修改数组长度

    可以使用 splice

    vm.array.splice(2)
    

    但 Vue 中的 splice 是被篡改过的,并不是 JS 中的数组的 splice。Vue 篡改过的方法,称为变异方法。

    数组变异方法有 7 个:push()pop()shift()unshift()splice()sort()reverse()

    数组新增修改最好通过这 7 个 API