大家好,我是前端GGBond,今天我们来聊一下vue2的响应式原理。
什么是响应式呢?
数据发生变化后,会重新对页面渲染,这就是Vue响应式,如下图
想完成这个过程,我们需要做些以下几点:
- 数据劫持 / 数据代理,侦测数据的变化
- 依赖收集,收集视图依赖了哪些数据
- 发布订阅模式,数据变化时,自动“通知”需要更新的视图部分,并进行更新
今天我们重点聊一下数据劫持的实现,以及vue在响应式监听中存在的问题,以及对应的解决方法。
一、针对对象的监听实现
前置知识、Object.defineProperty介绍(对象劫持)
定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
基本使用
语法:Object.defineProperty(obj, prop, descriptor)
2. prop,要定义或修改的属性的名称或 [Symbol]
前两个参数都很好理解,这里重点说一下第三个参数:属性描述符。
对象里目前存在的属性描述符,包括configurable、enumerable、value、writable、get、set。
而vue中针对对象的监听,主要是通过属性描述符的最后两个属性,get及set
属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
const obj = {
foo: ''
}
function update() {
console.log('obj.foo更新了', obj.foo)
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
调用defineReactive,数据发生变化触发update方法,实现数据响应式
defineReactive(obj, 'foo', '')
setTimeout(()=>{
obj.foo = new Date().toLocaleTimeString()
[]()},1000)
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
如果存在嵌套对象的情况,还需要在defineReactive中进行递归
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
}
}
上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题。
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
这些问题如何解决?我们后面再来讲。
二、针对数组的监听
首先说一下,vue针对数组的响应式并没有用到Object.defineProperty,主要原因有以下两点:
1.Object.defineProperty ,可以监听到数组属性变化,但因为性能消耗严重,所以成为废案。
其实Object.defineProperty,可以监听到数组属性变化,以下是测试代码:
let testArray = [0];
function test(data, key, val) {
Object.defineProperty(data, key, {
get() {
console.log(val);
},
set(newV) {
if (newV !== val) {
val = newV;
console.log('检测到变更');
}
},
});
}
test(testArray, 0, aa[0]);
testArray[0] = 1
网上其实很多人都在说Object.defineProperty是不能通过下标来修改数组的数据。但是自己测试怎么是可以检测到修改的?难道是他们说的都是错误的?
l 我们来看控制台输出,数据的长度是修改成功的,看来真的是他们的答案有误。
l 于是我继续寻找,终于找到了,确实不是Object.defineProperty()的问题,是vue本身做了限制,当数据是数组时,会停止对数据属性的监测,
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题。
所以, Object.defineProperty ****是有监控数组下标变化的能力的, 只是在 Vue2 的实现中,从性能/体验的性价比考虑,放弃了这个特性。
2.Object.defineProperty无法监听到数组api的变化
当我们对一个数组api进行监听的时候,发现Object.defineProperty并不那么好使了
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop() // no ok
[]()arrDate[0] = 99 // ok
所以在Vue2中,针对数组的监听没有用到Object.defineProperty,而是增加了set、delete API,并且对数组api方法进行一个重写。
与此同时,官网也提到,Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
小结
Vue 不能检测以下对象的变动:
Vue 不能检测以下数组的变动:
1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
2. 当你修改数组的长度时,例如:vm.items.length = newLength
针对这些不能监听变化的问题,vue肯定给出了解决方案,具体有哪些呢?
三、对象的手动更新
1.更新对象的单个属性——vue.set()
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。vue.set()这个api,其实就是做一次Object.defineProperty。语法是:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:
this.$set(this.someObject,'b',2)
2.更新对象的多个属性
有时你可能需要为已有对象赋值多个新 property(属性),比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 代替 Object.assign(this.someObject, { a: 1, b: 2 })
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
四、数组的手动更新
1.索引赋值更新无效的问题,例如vue.items[indexOfItem] = newValue
为了解决.items[indexOfItem] = newValue 更新无效的问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:
// Vue.set Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
2.watch中的deep监听
如果数组中带有对象,需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。所以,vue默认是不会做深度监听的。
但如果就是要深度监听怎么办呢?watch的专门有个deep的api,解决这个问题。
deep:代表深度监听,它有两个值分别是是true或false,不仅能监听到数组中对象的变化,也监听到该对象的属性变化。
3.修改数组的长度无法更新的问题,例如:vm.items.length = newLength
为了解决第二类问题,你可以使用 splice:
vm.items.splice(newLength)
五、另一种解决方法——$forceUpdate()手动刷新dom
示例:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。(说人话就是只更新自己这个组件,不会更新子组件)