浅析Vue数据响应式

113 阅读2分钟

什么数据是响应式

概念:响应即回应,假如一个物体由于外界的刺激而做出反应,那它就是响应式的。

那什么是数据响应式呢?

const vm = new Vue({data:{n:0}})
  • Vue的数据存储在data里,当修改vm.n时,会触发视图的渲染。这就是响应。

通过例子来分析

//引用完整版 Vue
import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;  //禁用警告

const myData = {
  n: 0
}
console.log(myData)  // 精髓

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

setTimeout(()=>{
  myData.n += 10
console.log(myData)  // 精髓
},3000)
  • 首先声明myDate对象,将n值设置为0,并打印出myDate
  • 将myDtae赋值给date,并且设置一个定时器,3s之后n值变为10,并再次打印出myDate
  • 打开控制台,观察myDate打印结果
  • 通过观察发现两次打印出的结果不同,说明vue对data里的数据进行了一些改造。

为了能够更好的理解其中的奥秘,我们需要理解以下知识点:

getter/setter

Object.defineProperty()

例:

import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;  //禁用警告

let obj = {
  姓: "小",
  名: "红",
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)//或者this.名 = xxx.substring(1)
  },
  age: 18
};

console.log(`姓 ${obj.姓},名 ${obj.名}`)
console.log(obj)
// obj打印结果
{姓: "小", 名: "红", age: 18}
age: 18
名: "红"
姓: "小"
姓名: (...)
get 姓名: ƒ 姓名()
set 姓名: ƒ 姓名(xxx)
__proto__: Object

说明:Vue通过setter 和 getter对myData的'n'进行改写,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

监听和代理

需求:无论我给n传的值是正是负,必须输出一个n大于0的值,

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

function proxy({data}){
  //监听data的变化 
  let value = data.n      // 声明一个新的 value 来获取 data.n ,监听 data.n 的变化
  Object.defineProperty(data, 'n', {   
    get(){ 
      return value
    },
    set(newValue){
      if(newValue<0)return
      value = newValue
    }
  })

 //添加代理
  const obj = {}// 声明一个新的对象,把 data.n 全权负责给 obj,这样无轮怎样篡改,
                // 都不会影响 n 的变化
  Object.defineProperty(obj, 'n', {  
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  return obj // obj 就是代理
}
console.log(`${data.n}`) // 结果:0
myData.n = -1
console.log(`${data.n},设置为 -1 失败了`) // 结果:0,设置-1失败
myData.n = 1
console.log(`${data.n},设置为 1 成功了`)// 结果:1,设置1成功
  • 声明一个myData对象,将n的值存进去
  • 声明一个data,让它成为myDta的代理,方便进行监听
  • 声明value来存储data.n的值
  • 使用Object.defineProperty,产生一个虚拟的属性n,如果读取n的值就调用get函数,如果设置n的值就,调用set函数,进行判断
  • 声明一个新的对象obj,让其成为代理,把 data.n 全权负责给 obj,这样无轮怎样篡改,都不会影响 n 的变化
  • 以上就是监听和代理的过程

Vue的bug解决

通过案例,我们知道要想实现监听和代理,需要使用Object.defineProperty(obj, 'n', {...}),但如果我忘记传'n',那就会有bug 例1:

import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;  //禁用警告

//引用完整版 Vue

Vue.config.productionTip = false;

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

浏览器报错

例2:

import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;  //禁用警告

//引用完整版 Vue

Vue.config.productionTip = false;

//引用完整版 Vue

Vue.config.productionTip = false;

new Vue({
  data: {
    obj: {
      a: 0 // obj.a 会被 Vue 监听 & 代理
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1; //请问,页面中会显示 1 吗?
    }
  }
}).$mount("#app");

当我点击set b按钮时,页面不会出现1。因为b根本不存在,所以你无法对其进行监听

解决方案一:提前申明好key

import Vue from "vue/dist/vue.js"; 

Vue.config.productionTip = false;  //禁用警告

//引用完整版 Vue

Vue.config.productionTip = false;

//引用完整版 Vue

Vue.config.productionTip = false;

new Vue({
  data: {
    obj: {
      a: 0
      b:''  //提前声明
      //或b:'undefined'  
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      this.obj.b = 1; //请问,页面中会显示 1 吗?
    }
  }
}).$mount("#app");

解决方案二:使用Vue.set或this.$set

//引用完整版 Vue
import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
    obj: {
      a: 0
    }
  },
  template: `
    <div>
      {{obj.b}}
      <button @click="setB">set b</button>
    </div>
  `,
  methods: {
    setB() {
      Vue.set(this.obj,'b',1)
      // this.$set(this.obj,'b',1)  //这句和上面等同
    }
  }
}).$mount("#app");

总结:由于Object.definePropery 的限制,Vue无法检测到对象属性的添加或删除。所以Vue不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值,或者使用Vue.set 或 vm.$set 进行设置。

数组的解决方案

使用Vue提供的变异方法操作

import Vue from "vue/dist/vue.js";

Vue.config.productionTip = false;

new Vue({
  data: {
    array: ["a", "b", "c"]
  },
  template: `
    <div>
      {{array}}
      <button @click="setD">set d</button>
    </div>
  `,
  methods: {
    setD() {
      this.array.push('d');
    }
  }
}).$mount("#app");

尤雨溪对着7个API进行了改造,方便对数组进行增删,这7个API会自动处理监听和代理,并更新视图。具体内容可以参考Vue文档

包括:

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