Vue数据响应式(代理和监控)

222 阅读3分钟

ES6的getter和setter

let obj0 = {
  姓: "高",
  名: "圆圆",
  age: 18
};

// 需求一,得到姓名

let obj1 = {
  姓: "高",
  名: "圆圆",
  姓名() {
    return this.姓 + this.名;
  },
  age: 18
};

console.log("需求一:" + obj1.姓名());
// 姓名后面的括号能删掉吗?不能,因为它是函数
// 怎么去掉括号?

// 需求二,姓名不要括号也能得出值

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

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

// 总结: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 函数

Object.defineProperty

(定义完getter和setter后忽然想加一个新的getter和setter时使用)

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法

Object.defineProperty(obj, prop, descriptor)

参数

  • obj 要定义属性的对象。
  • prop 要定义或修改的属性的名称或 Symbol 。
  • descriptor 要定义或修改的属性描述符。

示例

var _xxx = 0

Object.defineProperty(obj3, 'xxx', {
    get(){//因为已经给了xxx,所以这里不需要再get 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}`)

// 总结:这煞笔语法把事情搞复杂了?非也,继续看。

// 需求二:n 不能小于 0
// 即 data2.n = -1 应该无效,但 data2.n = 1 有效

let data2 = {}

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

Object.defineProperty(data2, 'n', {
  get(){
    return this._n//this表示当前这个对象,即data2
  },
  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 成功`)

// 抬杠:那如果对方直接使用 data2._n 呢?
// 算你狠

// 需求三:使用代理

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

function proxy({data}/* 解构赋值,别TM老问 */){
  const obj = {}
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  Object.defineProperty(obj, 'n', { 
    get(){
      return data.n
    },
    set(value){
      if(value<0)return
      data.n = value
    }
  })
  return obj // obj 就是代理
}

// data3 就是 obj
console.log(`需求三:${data3.n}`)
data3.n = -1
console.log(`需求三:${data3.n},设置为 -1 失败`)
data3.n = 1
console.log(`需求三:${data3.n},设置为 1 成功`)

// 杠精你还有话说吗?
// 杠精说有!你看下面代码
// 需求四

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

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

// 我现在改 myData,是不是还能改?!你奈我何
// 艹,算你狠

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

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

function proxy2({data}/* 解构赋值,别TM老问 */){
  // 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
  // 因为我怕你们看不懂
  let value = data.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 就是代理
}

// data3 就是 obj
console.log(`需求五:${data5.n}`)
myData5.n = -1
console.log(`需求五:${data5.n},设置为 -1 失败了`)
myData5.n = 1
console.log(`需求五:${data5.n},设置为 1 成功了`)


// 这代码看着眼熟吗?
// let data5 = proxy2({ data:myData5 }) 
// let vm = new Vue({data: myData})

// 现在我们可以说说 new Vue 做了什么了

小结

vue 对 data 做了什么?

vm = new Vue({data: myData})

  1. 会让vm成为myData的代理(proxy),可以通过this来访问vm(this.n来访问myData.n,因为vm是代理)

  2. 会对myData的所有属性进行监控,为了防止myData的属性变了,vm不知道。

    vm知道属性变了就可以调用render(data)来更新 UI 和渲染页面,UI = render(data)

  3. const vm = new Vue({data: {n:0}})整个流程:vue会对内部数据data进行改造和监听,let value=0并且对其get和set形成新的被篡改后的对象,然后通过代理对篡改后的对象进行读和写,再用vm接纳新的对象,data 中有多个变量/属性时,可以用闭包和循环来实现这个过程。

代理和监听可以在不论进行了任何修改的情况下都能被vue得知,从而进行页面更新,防止错漏

示意图

image.png

数据响应式

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

Vue的data是响应式,const vm = new Vue({data: {n: 0}}},如果修改vm.n,那么UI中的n就会响应我

Vue 2通过0bject.defineProperty来实现数据响应式

Vue有一个bug

Object.defineProperty的问题

Object.defineProperty(obj, 'n' ,{..})必须要有一个'n',才能监听&代理obj.n

示例1. 如果没有给n

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

Vue.config.productionTip = false;

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

Vue会给一个警告n没有被定义

image.png

示例2.Vue只会检查第一层

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

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");

页面中不会显示1,因为b一开始没有被监听

解决方法有2个

  1. 一开始声明好所有的key
new Vue({
  data: {
    obj: {
      a: 0 ;
      b:undefined
 }
 }

再点击setb就会显示1

  1. 使用Vue.set 和 this.$set
methods: {
 setB() {
 写法1:Vue.set(this.obj,'b',1) 
 或者写法2this.$set(this.obj,'b',1)
    }
 }

作用:

  • a.新增key

  • b.自动创建代理和监听(如果没有创建过)

  • c.触发UI更新(但并不会立刻更新)

示例:当data有数组时

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[3] = "d"; //请问,页面中会显示 'd' 吗?
      // 等下,你为什么不用 this.array.push('d')
    }
  }
}).$mount("#app");

不会显示d是因为一开始数组的下标只有0,1,2;数组的长度无法预测无法使用 undefined 去为每一项占位,或一直使用 Vue.set( ) 方法。

因此可以用 Array.push 方法来添加

this.array.push('value')

Array.push不是原生的API,而是当你将数组对象传递给vue后,它会篡改数组,在中间增加一层原型,该原型有七个方法:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse() 这些API会自动处理对数组的监听和代理,并触发视图更新。

大概原理:

c1ass VueArray extends Array {
    push(...args) {
       const oldLength = this.length // this 就是当前的数组
       super.push(...args)//原理会先调用父级的push,但是会执行后面的监听代码
       console.1og('你push 了')
       for(let i = oldLength; i<this.length; i++){
       Vue.set(this, i, this[i])//将每个新增的key都告诉Vue
      }
}
const a = new VueArray(1,2,3,4)
a. push(5)

总结

对象中新增的key

  • Vue没有办法事先监听和代理
  • 要使用set 来新增key,创建监听和代理,更新Ul
  • 最好提前把属性都写出来,不要新增key
  • 但数组做不到「不新增key」

数组中新增的key

  • 也可用set来新增key,但set对于数组无法创建监听和代理,更新UI
  • 不过尤雨溪篡改了7个API方便你对数组进行增删
  • 这7个API会自动处理监听和代理,并更新U
  • 结论:数组新增key最好通过7个API、

我对Vue的数据响应式的理解

Vue在创建实例时,一个普通的JS对象会传给data选项,修改这个对象的属性,视图会自动更新。这就是Vue的数据响应式。

Vue是通过Object.defineProperty来实现数据响应式的。Vue在初始化实例时会对data选项里的所有属性先保存原始值,再设置有getter,setter的同名属性覆盖原来的属性。通过getter,setter对属性进行监听,只要监听到这个属性改变了,就去重新渲染视图。

但是,Object.defineProperty必须要有一个属性,才能监听它。也就是说,Vue只能监听初始化实例时就已经存在于data中的属性,如果之后想添加新的属性并且改变它的值,Vue是无法对它进行监听的,也就不会触发视图更新。这就是Vue的data的一个bug。

解决这个bug有两种方法。可以预先定义好所有的属性,先设为undefined。但是更好的做法是使用Vue.set或this.$set方法。比如:Vue.set(this.obj,'b',1) 但是要注意,Vue不允许在已经创建的实例上动态添加新的根级响应式属性,使用Vue.set只能将响应式属性添加到嵌套的对象上。Vue.set不仅在实例上添加属性,也会自动创建监听和代理,然后更新视图。

对于数组来说,无法提前定义好全部的属性。所以尤雨溪篡改了数组的7个API。原理就是,在数组对象和Array构造函数之间加了一个原型,这个原型上重新定义了7个API。以push为例,使调用arr.push时,先单纯的push进数组,然后添加监听和代理。