Vue官网:当把一个普通的对象传入Vue实例作为data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些数星星全部转化为getter/setter。Object.defineProperty是ES5中一个无法shim的特性(查了一下shim的意思:就是可以将新的API引入到旧的环境中,而且仅靠环境中的已有手段实现,Obejct.defineProperty无法在低级浏览器中的方法实现),这也就是Vue不支持IE8以及更低版本浏览器的原因。

一、Vue无法检测对象属性添加或删除

Object.defineProperty 没有对对象的新属性进⾏属性劫持

原因:由于Vue会在初始化实例时对对象的属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。

1
2
3
4
5
6
7
8
var vm = new Vue({
data:{
a:1
}
})

vm.a 是响应式
vm.b 是非响应式

解决无法检测对象属性的增加、删除的方法:Vue.set(object,propertyName,value)(增加属性)、Vue.delete()(删除属性)

二、Vue不能检测数组的变动

数组是一个特殊的JavaScript对象,Vue没有对数组进⾏ Object.defineProperty 的属性劫持,所以会存在的问题:

不能检测数组变动的2类问题:

1、当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
2、当你修改数组的长度时,例如:vm.items.length = newLength

1
2
3
4
5
6
7
8
var vm = new Vue({
data:{
items: ['a'','b','c']
}
})

vvm.items[1] = 'x' // 非响应式
vm.items.length = 2 // 非响应式

解决利用索引值设置一个数组项的方法:

vm.$set(vm.items, indexOfItem, newValue)

解决第2个问题:

vm.items.splice(newLength) // Array.prototype.slice

三、Vue2 为什么不劫持数组?

Vue2使用的Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应; Object.defineProperty是可以监听数组下标变化的,对于javascript来讲,数组也是属于Object。

尤雨溪说过:为了性能!!!

原因:

1、因为数组的位置不固定,数量多变,正常对象key对应value一般不会变,但是如果数组删除了某个元素。比如第一个元素被删除或者头部增加一个元素,那么将导致后面所有的key对应value错位,如果6个元素,也就会触发5次set。
2、数组元素可能非常非常多,每个元素进行劫持有一定浪费,这可能是Evan you对性能的考虑。
3、Vue将数组的7个变异方法进行了重写,也就是更改了Array原型上的方法达到劫持变化。

Vue3 的 proxy
Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。

Proxy可以劫持整个对象,并返回一个新的对象。

所以为什么proxy优于Object.defineProperty?

Object.defineProperty必须“预先”劫持属性。被劫持的属性才会被监听到。所以后添加的属性,需要手动再次劫持。

而proxy代理了整个对象,不需要预先劫持属性,而是在获取/修改的时候,通过get/set方法来告诉你key。所以不管如何新增属性,总是能被捕获到。

四、Vue响应式原理

整体思路是数据劫持+观察者模式:

对象内部通过 defineReactive ⽅法,使⽤ Object.defineProperty 将属性进⾏劫持(只会劫持已经存在的属性),数组则是通过重写数组⽅法来实现。当⻚⾯使⽤对应属性时,每个属性都拥有⾃⼰的 dep 属性,存放他所依赖的 watcher (依赖收集),当属性变化后会通知⾃⼰对应的 watcher 去更新(派发更新)。

Vue响应式原理的核心:就是Observer、Dep、Watcher。Observer中进行响应式的绑定,在数据被读的时候,触发get方法,执行Dep来收集依赖,也就是收集Watcher。在数据被改的时候,触发set方法,通过对应的所有依赖(Watcher),去执行更新。比如watch和computed就执行开发者自定义的回调方法。

五、为什么只对对象劫持,对数组方法进行重写?

因为对象最多也就⼏⼗个属性,拦截起来数量不多,但是数组可能会有⼏百⼏千项,拦截起来⾮常耗性能,所以直接重写数组原型上的⽅法,是⽐较节省性能的⽅案

六、实现一个数据双向绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<input id="hangdleInput" type="text">
<span id="hangdleValue"></span>
<script>
let obj ={}
let input = document.getElementById('hangdleInput')
let span = document.getElementById('hangdleValue')
Object.defineProperty(obj,'text',{
configurable: true,
enumerable: true,
get(){
console.log('get data')
},
set(newVal){
console.log('set data')
input.value = newVal
span.innerHTML = newVal
}
})
input.addEventListener('keyup',function(e){
obj.text = e.target.value
})
</script>

七、Vue3 Proxy

Vue2 的响应式是基于 Object.defineProperty 实现的
Vue3 的响应式是基于 ES6 的 Proxy 来实现的

在Vue2.0中,数据双向绑定就是通过Object.defineProperty去监听对象的每一个属性,然后在get/set方法中通过发布订阅者模式来实现的数据响应,但是存在一定的缺陷,比如只能监听已存在的属性,对于新增删除属性就无能为力了,同时无法监听数组的变化,所以在Vue3.0中将其换成了功能更强大的ES6 Proxy。

Vue2的响应式函数:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 响应式函数
function reactive(obj, key, value) {
Object.defineProperty(data, key, {
get() {
console.log(`访问了${key}属性`)
return value
},
set(val) {
console.log(`将${key}由->${value}->设置成->${val}`)
if (value !== val) {
value = val
}
}
})
}
const data = {
name: 'jude',
age: 18
}
Object.keys(data).forEach(key => reactive(data, key, data[key]))
console.log(data.name)
// 访问了name属性
// jude
data.name = 'summer' // 将name由->jude->设置成->summer
console.log(data.name)
// 访问了name属性
// summer
```

Vue3的响应式函数:
```js
const data = {
name: 'jude',
age: 18
}

function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log(`访问了${key}属性`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`将${key}由->${target[key]}->设置成->${value}`)
Reflect.set(target, key, value, receiver)
}
}

return new Proxy(target, handler)
}

const proxyData = reactive(data)

console.log(proxyData.name)
// 访问了name属性
// jude
proxyData.name = 'summer'
// 将name由->jude->设置成->summer
console.log(proxyData.name)
// 访问了name属性
// summer
1
2
3
4
5
6
/**
* target: 要兼容的对象,可以是一个对象,数组,函数等等
* handler: 是一个对象,里面包含了可以监听这个对象的行为函数,比如上面例子里面的get与set
* 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值
*/
const proxy = new Proxy(target, handler)

1、handle.get、handle.set、handle.has、handler.deleteProperty

当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面
当通过proxy去为对象设置修改属性的时候,会进入到set钩子函数里面
当使用in判断属性是否在proxy代理对象里面时,会触发has
当使用delete去删除对象里面的属性的时候,会进入deleteProperty钩子函数

2、Reflect

在上面,我们获取属性的值或者修改属性的值都是通过直接操作target来实现的,但实际上ES6已经为我们提供了在Proxy内部调用对象的默认行为的API: Reflect。

1
2
3
4
5
6
const obj = {}
const proxy = new Proxy(obj, {
get(target,key,receiver) {
return Reflect.get(target,key,receiver)
}
})