谈谈Vue的数据响应式

1,026 阅读2分钟

引子

先来一个例子

//main.js
import Vue from 'vue'

Vue.config.productionTip = false;

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

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

setTimeout(()=>{
  myData.n += 10
  console.log(myData) 
},5000)

按照常理来说,第一次打印应该得到{n:0},第二次应该是{n:10}才对,

经过验证,第一次打印如下:

QQ图片20220105215349

myData.n += 10传给new Vue之后,再次点开{n:0}

QQ图片20220105203217

这是为啥?

带着这个疑问,开启本文

getter和setter

再来个例子

let obj0 = {
  姓: "周",
  名: "杰伦",
  age: 18
};

需求一:得到姓名

很简单

let obj1 = {
  姓: "周",
  名: "杰伦",
  姓名() {
    return this.姓 + this.名;
  },
  age: 18
};

console.log("需求一:" + obj1.姓名());

需求二:我就是不想要obj1.姓名()怎么办?

也不是不行

let obj2 = {
  姓: "周",
  名: "杰伦",
  get 姓名() {
    return this.姓 + this.名;
  },
  age: 18
};

console.log("需求二:" + obj2.姓名);

OK,至此我们就已经了解了getter是怎么用的,不加括号的函数,仅此而已。

需求三:姓名可以被改写

let obj3 = {
  姓: "周",
  名: "杰伦",
  get 姓名() {
    return this.姓 + this.名;
  },
  set 姓名(xxx){
    this.姓 = xxx[0]
    this.名 = xxx.slice(1)
  },
  age: 18
};

obj3.姓名 = '周星驰'

console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)

setter 就是这样用的。用 = xxx 触发 set 函数,即用计算属性就能修改原始属性

QQ图片20220105204358

我们再console.log(obj3)

QQ图片20220105204735

会发现这个姓名:(...)跟之前的n:(...)很相近

但我们并没有一个属性叫做姓名,所以浏览器在显示姓名的时候是以姓名:(...)的方式显示的

就是说,你确实可以对姓名进行读和写,但并不存在一个叫做姓名的属性,读和写是通过下面的get 姓名set 姓名来完成的

这说明我们之前得到的n:(...)也是同理,有一个get nset n来模拟对n的读写操作

但为啥要把n变成get nset n呢?

Object.defineProperty

如果我们想在已经声明完的对象添加一个新get/set,就需要Object.defineProperty

如,在上述的obj3上添加

var _xxx = 0

Object.defineProperty(obj3,'xxx',{
	get(){
      	return _xxx
    },
    set(value){
        _xxx = value
    }
})

我们接着举例子

let data0 = {
  n: 0
}

需求一:用 Object.defineProperty定义 n

let data1 = {}

Object.defineProperty(data1, 'n', {
  value: 0
})

console.log(`需求一:${data1.n}`)//打印出:需求一:0

需求二:n 不能小于 0

let data2 = {}

data2._n = 0 // _n 用来偷偷存储 n 的值

Object.defineProperty(data2, 'n', {
  get(){
    return this._n
  },
  set(value){
    if(value < 0) return
    this._n = value
  }
})

console.log(`需求二:${data2.n}`)
data2.n = -1
console.log(`需求二:${data2.n} 设置为 -1 失败`)
data2.n = 1
console.log(`需求二:${data2.n} 设置为 1 成功`)

QQ图片20220105211020

通过这个例子,可以知道set里面是可以做判断的

这时,来了一个杠精:“那如果别人直接使用 data2._n 呢?”

那确实没办法,所以

需求三:使用代理

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

function proxy({data}){
  const obj = {}
  Object.defineProperty(obj, 'n', { 
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  return obj // obj 就是代理
}

// data3 就是 obj

这么一来proxy({ data:{n:0} })就没办法被修改了 ,

且对obj的n做什么,同时,就会对data的n做什么

但杠精仍不服,他又给出了以下代码

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

// data3 就是 obj
console.log(`杠精:${data4.n}`)
myData.n = -1
console.log(`杠精:${data4.n},设置为 -1 成功!`)

QQ图片20220105212049

需求四:就算用户擅自修改 myData,也要拦截他

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

function proxy2({data}){
  let value = data.n
  delete data.n//把原始数据的n删掉,可以不写,再下面声明新的n时,会把旧的n覆盖
  Object.defineProperty(data, 'n', {
    get(){
      return value
    },
    set(newValue){
      if(newValue<0)return
      value = newValue
    }
  })
  // 加了上面几句,这几句话会监听 data

  const obj = {}
  Object.defineProperty(obj, 'n', {
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  
  return obj // obj 就是代理
}

这样一来,就能100%地防止数据在不知情的时候被修改

接下来做一个类比:

  • let data5 = proxy2({ data:myData5 })
  • const vm = new Vue({data:{n:0}})

会发现很接近,其实上面例子的原理,就是Vue内部用到的原理(只是原理不是代码)

小结

Object.defineProperty

  • 可以给对象添加属性value
  • 可以给对象添加getter/setter
  • getter/setter用于对属性的读写进行监控

代理

  • 对myData对象的属性读写,全权由另一个对象vm负责
  • 那么vm就是myData的代理

vm=new Vue({data:myData})

  1. 会让vm成为myData的代理
  2. 会对myData的所有属性进行监控
    • 为什么要监控?为了防止myData的属性变了,vm却不知道
    • vm知道了能干嘛?知道了属性变了就可以调用render(data),UI就能够自动刷新~

总而言之就是,你对data做的任何修改,我Vue就必须知道,就是这么霸道

所以,在引子中,只要把 myData传给new Vue,new Vue就马上会对myData进行一个篡改,之前的n就不见了,取而代之的是一个get nset n

数据响应式

什么是响应式?

  • 你的女朋友给你一耳光,你会喊疼,那你就是响应式的

  • 总之,一个物体能对外界的刺激做出反应,它就是响应式的

Vue的data是响应式

  • const vm = new Vue({data:{n:0}})
  • 我修改vm.n,那么UI中的n就会响应我
  • Vue通过Object.defineProperty来实现数据响应式