Vue.js原理之vue响应式/怎样监听data变化
- 组件data的数据一旦发生变化,立刻触发视图更新。
- Vue.js实现响应式的关键 ⇒ 核心API ⇒
Object.defineProperty(3.0前,Vue.js3.0启用的事Proxy,但是Proxy有兼容性问题)。 - Object.defineProperty()缺点(这些缺点只是几个知识点而已,并不是在放大它们。下方示例中会有说明):
- (1)深度监听需要递归到底,一次性计算量大。
- (2)无法监听新增/删除属性(Vue.js2.0中对象属性的新增/删除一定要用Vue.set/Vue.delete)。
- (3)无法原生监听数组,需要特殊处理。
示例一:展示最基本的数据响应式
<script type="text/javascript">
//触发更新视图
function updateView(){
console.log('视图更新')
}
//重新定义属性,监听起来
function defineReactive(target, key, value){
//核心API
Object.defineProperty(target, key, {
get(){
return value;
},
set(newValue){
if(newValue !== value){
//设置新值
//注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue
//触发更新视图
updateView();
}
}
})
}
//监听对象属性
function observer(target){
if(typeof target !== 'object' || target === null){
//不是对象或数组
return target;
}
//重新定义各个属性(for in 也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
//准备数据
const objA= {
name:'zhangsan',
age:20,
// info:{
// address:'北京',
// },
// nums:[10, 20, 30]
}
//监听数据
observer(objA);
//测试
data.name = 'list';
data.age = 21;
//data.x = '100';//新增属性,监听不到,所以有vue.set
//delete data.name;//删除属性,监听不到,所以有vue.delete
//data.info.address = '上海';//深度监听
//data.nums.push(4)//监听数组
</script>
这个示例会在控制台输出两次’视图更新’,
在observer函数中监听对象objA的变化,遍历对象,监听每个属性的变化,因为测试时给objA的name属性个age属性都重新赋值,所以都会触发updateView()更新视图,因此控制台会输出两次’视图更新’。
示例二:对象深度监听
还是示例一中的代码,这时我们把对象objA的info属性解开注释
const objA = {
name:'zhangsan',
age:20,
info:{
address:'北京',
},
// nums:[10, 20, 30]
}
data.info.address = '上海';//深度监听
控制台依旧还是输出两次’视图更新’。这时的defineReactive函数监听不到info.address的变化,因为这个函数只做了单层级的监听,需要在defineReactive函数中再调用一次observer函数。
//重新定义属性,监听起来
function defineReactive(target, key, value){
observer(value)
//核心API
Object.defineProperty(target, key, {
get(){
return value;
},
set(newValue){
if(newValue !== value){
//设置新值
//注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue
//触发更新视图
updateView();
}
}
})
}
此时当oberver函数中传入info:{address:‘北京’}时,因为是对象类型所以不走if判断,又做了一遍forin循环。
此时会在控制台输出三次’视图更新’。
此时如果改变objA对象的age属性值,从数值型换成对象类型,会是什么结果呢。
objA.age = {num:21};
此时会在控制台输出四次’视图更新’。
然而当再次修改时,objA.age.num=22; 控制台依然是四次’视图更新’输出。监听并没有监测出来objA.age.num的变化。
要解决这个问题,必须在核心APIObject.defineProperty的set函数中再调用一次oberserve监听对象函数,一次递归到底。
function defineReactive(target, key, value){
observer(value);//深度监听
//核心API
Object.defineProperty(target, key, {
get(){
return value;
},
set(newValue){
if(newValue !== value){
observer(newValue);//深度监听
//设置新值
//注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue
//触发更新视图
updateView();
}
}
})
}
这时候控制台会输出五次’视图更新’。
示例三:js原生方法新增/删除属性
如果给对象新增一个属性,或删除一个属性,这个时候是Object.defineProperty()是监听不到的。
objA.x = ‘100’;//新增属性,监听不到,所以有vue.set
delete objA.name;//删除属性,监听不到,所以有vue.delete
示例四:监听数组的变化
解开对象objA中的num属性
const objA = {
name:'zhangsan',
age:20,
info:{
address:'北京',
},
nums:[10, 20, 30]
}
测试部分只留objA.nums.push(4)//监听数组,其它都注释掉。
此时代码执行虽然能进入observer函数进行遍历,但是Object.defineProperty()不具备监听数组的能力。
此时需要在代码上面重新定义数组的原型,为防止污染数组原有的原型,所以要重新定义数组原型。见下方代码
//重新定义数组原型
const oldArrayProperty = Array.prototype;
//创建新对象,原型指向oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function(){
updateView();//触发更新视图
oldArrayProperty[methodName].call(this, ...arguments);
}
})
还需要把observer函数做一个判断类型是否是数组的分支
//监听对象属性
function observer(target){
if(typeof target !== 'object' || target === null){
//不是对象或数组
return target;
}
//类型是数组走这个分支
if(Array.isArray(target)){
target.__proto__ = arrProto;
}
//重新定义各个属性(for in 也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
下方是完整代码
<script type="text/javascript">
//触发更新视图
function updateView(){
console.log('视图更新')
}
//重新定义数组原型
const oldArrayProperty = Array.prototype;
//创建新对象,原型指向oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function(){
updateView();//触发更新视图
oldArrayProperty[methodName].call(this, ...arguments);
}
})
//重新定义属性,监听起来
function defineReactive(target, key, value){
observer(value);//深度监听
//核心API
Object.defineProperty(target, key, {
get(){
return value;
},
set(newValue){
if(newValue !== value){
observer(newValue);//深度监听
//设置新值
//注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue
//触发更新视图
updateView();
}
}
})
}
//监听对象属性
function observer(target){
if(typeof target !== 'object' || target === null){
//不是对象或数组
return target;
}
//类型是数组走这个分支
if(Array.isArray(target)){
target.__proto__ = arrProto;
}
//重新定义各个属性(for in 也可以遍历数组)
for(let key in target){
defineReactive(target, key, target[key])
}
}
//准备数据
const objA = {
name:'zhangsan',
age:20,
info:{
address:'北京',
},
nums:[10, 20, 30]
}
//监听数据
observer(objA);
//测试
// objA.name = 'list';
// objA.age = 21;
//objA.x = '100';//新增属性,监听不到,所以有vue.set
//delete objA.name;//删除属性,监听不到,所以有vue.delete
// objA.info.address = '上海';//深度监听
objA.nums.push(4)//监听数组
// objA.age = {num:21};
// objA.age.num=22;
</script>