vue数据响应式

问题引入:Vue对data做了什么

data值变了,例子中 myData 在传入前和传入后,两次log出的结果不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const myData = {
n: 0
}
console.log(myData) // 第一次log出的结果为 { n: 0 }

const vm = new Vue ({
data: myData,
template: `
<div>
{{ n }}
<button @click="add"> +10 </button>
</div>
`,
methods: {
add() {
this.n += 10
}
}
}).$mount("#app")

setTimeout(() => {
myData.n += 10
console.log(myData) // 第二次log出的结果为 { n:(...) }
}, 3000)

为什么两次log出的myData会不一致?需要先了解一下ES6中的getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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.姓名); // 在函数名前加上get,在调用时函数可以不加括号

// 需求三:函数可以被“写”
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx) {
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
}
obj3.姓名 = "高媛媛" // set的用法

console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
console.log(obj3)

  • 虽然在声明obj3时并未声明 姓名 这样的属性,但你确实可以对它进行读、写的操作,打印出的结果为 姓名: (…)
  • n: (…) 的意思就是并不存在n这样的属性,但是它身上有一个 get n 和 set n 来模拟对n的读写操作

Object.defineProperty()

  • 定义完一个对象后,想要在它身上再添加额外的getter和setter时使用Object.defineProperty()
  • 可以给对象添加属性value,也可以添加getter、setter
  • getter、setter用于对属性的读写进行监控
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let _xxx = 0  // 声明一个容器,用来存放 xxx 的值

    Object.defineProperty(obj1, 'xxx', {
    get() {
    return _xxx
    },
    set(value) {
    _xxx = value
    }
    }) // 给obj1增加了一个名为 xxx 的属性,值为0

使用Object.defineProperty()完成几个需求

需求一:用Object.defineProperty()定义n的值

1
2
3
4
5
6
7
8
9
let data0 = {
n: 0
}

let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
console.log(`需求1:${data1.n}`)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let data2 = {}

data2._n = 0 // 用来存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求2:${data2.n}`)

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

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

// 问题:虽然这样可以满足需求,但如果有人直接修改data2._n的值怎么办?

使用代理,解决上述问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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就是代理:对obj做什么,就会同样作用在 data3 身上
}

// data3 就是 obj
console.log(`需求3: ${data3.n}`);

data3.n = -1
console.log(`需求3 : ${data3.n} 设置为-1 失败`);

data3.n = 1
console.log(`需求3 : ${data3.n} 设置为1 成功`);

需求四:解决绕过代理的问题,就算有人修改myData,也要拦截他

下面是可以被用户修改的myData

1
2
3
4
5
6
let myData = { n:0 }  // 声明一个引用,这样就可以绕过代理,直接修改myData
let data4 = proxy({ data: myData }) // 括号里是匿名对象,无法访问

console.log(`杠精:${data4.n}`)
myData.n = -1 // 绕过代理,成功修改了myData
console.log(`杠精:${data4.n}, 设置为-1成功了`)

解决绕过代理的问题,不要让用户擅自修改myData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 })

function proxy2({data}) {
let value = data.n
// 省略了delete.data.n,使用Object.defineProperty时会覆盖掉
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 对 data 进行代理
console.log(`需求4:${data4.n}`)

myData5.n = -1
console.log(`需求4:${data4.n} 设置为 -1 失败`)

myData5.n = 2
console.log(`需求4:${data4.n} 设置为 1 成功`)

什么是代理

  • 对 myData 对象的属性进行读写,全权由另一个对象vm来负责
  • 那么,vm 就是 myData 的代理
  • 不用 myData.n 偏偏用 this.n 或 vm.n

    new Vue做了什么

    1
    2
    3
    // 看着很相似的两行代码
    let data4 = proxy({data: myData4})
    let vm = new Vue({data: myData})
  • 会让vm成为myData的代理,可以通过 this.myData 访问到 myData

  • 会监听 myData 的所有属性(赋值给value,再通过getter、setter添加虚拟属性)
  • 监听的目的是防止 myData 属性改变,vm不知道
  • 当vm监测到myData发生改变,就调用render(data),刷新视图

Vue的数据响应式

Vue 的 data 是响应式的

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

    Vue 的 data 的bug

    问题:如果一开始在data中并未对 b 进行赋值,Vue就不能对它进行代理或监听
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    new Vue({
    data: {
    obj: {
    a: 0 // obj.a 会被Vue监听和代理
    }
    },
    template: `
    <div>
    {{ obj.b }}
    <button @click="setB">set b</button>
    <button @click="addB">b+1</button>
    </div>
    `,
    methods: {
    setB(){
    this.obj.b = 1 // 这种写法页面中不会显示b的值,因为Vue没办法监听一开始就未设置在data中的obj.b

    // 解决方法,使用Vue.set 或 this.$set
    console.log(Vue.set === this.$set) // 下面两种写法等价
    Vue.set(this.obj, 'b', 1)
    this.$set(this.obj, 'b', 1)
    },
    addB() {
    this.obj.b += 1
    }
    }
    }).$mount("#app")

解决方法:使用 Vue.set 或 this.$set,作用是:

  • 新增key
  • 自动创建监听和代理
  • 触发ui进行更新(但并不是立即更新)

eg:this.$set(this.object, ‘m’, 10)

如果data是一个数组呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Vue({
data: {
array: ['a', 'b', 'c'] // array: {0:'a', 1:'b', 2:'c'}
},
template: `
<div>
{{array}}
<button @click="setD">setD</button>
</div>
`,
methods: {
setD() { // 3种 setD 的方法
this.array[3] = 'd' // 页面中不会出现'd' Vue不能监测到新增的d
this.$set(this.array, 3, 'd') // 可以用$set的方法,但每次都用set会不会很麻烦
this.array.push('d') // 这个push方法被尤雨溪篡改了,有两个作用:一是调用以前的push方法,二是增加代理和监听
}
}
})

当你将一个数组传给Vue的data,Vue会对这个数组进行篡改,它会在原来数组对象中间增加一层原型,这个原型上有7个方法

来看看push是如何被篡改的?

1
2
3
4
5
6
7
8
9
10
class VueArray extends Array {
push(...args) {
const oldLength = this.length // this就是当前数组
super.push(...args)
console.log('你 push 了')
for(let i = oldLength; i<this.length; i++){ // i为所有push的元素的下标,遍历旧数组到新数组的下标
Vue.set(this, i, this[i]) // 在当前数组this上,添加i项,每项i的值为this[i],并将每个新增的key通过Vue.set告知给Vue
}
}
}

总结

对象中新增的key

  • Vue没办法事先监听和代理
  • 要使用 Vue.set / this.$set 来新增key,创建监听和代理、更新ui
  • 最好的方式是将属性都写出来,不要新增key。但数组没办法做到不新增key

    数组中新增的key

  • 可以用 Vue.set / this.$set 来新增key,创建监听和代理、更新ui
  • 但尤玉溪篡改了数组的7个API方便对数组进行增删
  • 这7个API会自动处理监听和代理、更新ui
0%