Vue双向数据绑定原理
vue2
defineProperty
第一个参数为需要定义属性的对象
第二参数为需要定义的属性名
第三个参数为属性描述符
使用definePropery定义的属性不能对其进行修改,删除,以及枚举。
怎么解决呢?
在属性描述符中设置
writable:true 可修改
enumerable:true 可枚举
configurable:true 可操作/删除
var obj={}
Object.defineProperty(obj,"name",{
value:"lisa",
writable:true,
enumerable:true,
configurable:true
})
defineProperty的set和get属性
用于实现数据劫持
var obj={}
var a=1;
Object.defineProperties(obj,{
a:{
// 注意:value,writable属性与get,set互斥,如果同时存在则会报错
//所以,设置初始值只能在外部设置
//在a被访问时返回a的值
get(){
console.log("a被访问了,值为"+a)
return a;
},
//在a被修改时,将a的值修改为传入的新值
set(newVal){
a=newVal;
console.log("a被修改了,值为"+a)
}
}
})
obj.a //访问a,值为1
obj.a=5 //设置a的值,值为5
obj.a//再次访问a,值为5
将操作的元素放入数组
function DataArr(){
var Arrobj={} //数组的属性
var arr=[]
//this指向new DataArr()
Object.defineProperty(this,"val",{
get(){
return Arrobj;
},
set(newVal){
Arrobj=newVal
//向数组中添加新的属性值
arr.push({val:Arrobj})
}
})
this.getArr=function(){
return arr
}
}
var dataArr=new DataArr()
dataArr.val=123
dataArr.val=456
console.log(dataArr.getArr()) //[{val:123},{val:456}]
问题
关于对象: Vue 无法检测 property 的添加或移除。
关于数组:不能利用索引直接设置一个数组项,也不能修改数组的长度。
vue3
原理:Vue3.0中的响应式采用了ES6中的 Proxy 方法。Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)
据阮一峰文章介绍:Proxy可以理解成,在目标对象之前架设一层 "拦截",当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。
- 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法)。
- 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。
const obj = new Proxy(target, handler);
target: 被代理对象。
handler: 是一个对象,声明了代理target的一些操作。
obj: 是被代理完成之后返回的对象。
但是当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:
1. get(target, propKey, receiver)
2. set(target, propKey, value, receiver)
3. has(target, propKey)
4. construct(target, args):
5. apply(target, object, args)
const target = {
name: 'kongzhi'
};
const handler = {
get: function(target, key) {
console.log("${key} 被读取");
return target[key];
},
set: function(target, key, value) {
console.log("${key} 被设置为 ${value}");
target[key] = value;
}
};
const testObj = new Proxy(target, handler);
/*
获取testObj中name属性值
会自动执行 get函数后 打印信息:name 被读取 及输出名字 kongzhi
*/
console.log(testObj.name);
/*
改变target中的name属性值
打印信息如下: name 被设置为 111
*/
testObj.name = 111;
console.log(target.name); // 输出 111
如上代码所示:也就是说 target是被代理的对象,handler是代理target的,那么handler上面有set和get方法,当每次打印target中的name属性值的时候会自动执行handler中get函数方法,当每次设置 target.name 属性值的时候,会自动调用 handler中的set方法,因此target对象对应的属性值会发生改变,同时改变后的 testObj对象也会发生改变。同理改变返回后 testObj对象中的属性也会改变原对象target的属性的,因为对象是引用类型的,是同一个引用的。如果这样还是不好理解的话,可以简单的看如下代码应该可以理解了:
const target = {
name: 'kongzhi'
};
const testA = target;
testA.name = 'xxx';
console.log(testA.name); // 打印 xxx
console.log(target.name); // 打印 xxx
get
该方法的含义是:用于拦截某个属性的读取操作。它有三个参数,如下解析:
target: 目标对象。
propKey: 目标对象的属性。
receiver: (可选),该参数为上下文this对象
const obj = {
name: 'kongzhi'
};
const handler = {
get: function(target, propKey) {
// 使用 Reflect来判断该目标对象是否有该属性
if (Reflect.has(target, propKey)) {
// 使用Reflect 来读取该对象的属性
return Reflect.get(target, propKey);
} else {
throw new ReferenceError('该目标对象没有该属性');
}
}
};
const testObj = new Proxy(obj, handler);
/*
Proxy中读取某个对象的属性值的话,
就会使用get方法进行拦截,然后返回该值。
*/
console.log(testObj.name); // kongzhi
/*
如果对象没有该属性的话,就会进入else语句,就会报错:
Uncaught ReferenceError: 该目标对象没有该属性
*/
// console.log(testObj.name2);
/*
其实Proxy中拦截的操作是在原型上的,因此我们也可以使用 Object.create(obj)
来实现对象的继承的。
如下代码演示:
*/
const testObj2 = Object.create(testObj);
console.log(testObj2.name);
// 看看他们的原型是否相等
console.log(testObj2.__proto__ === testObj.__proto__); // 返回true
该方法是用来拦截某个属性的赋值操作,它可以接受四个参数,参数解析分别如下:
target: 目标对象。
propKey: 目标对象的属性名
value: 属性值
receiver(可选): 一般情况下是Proxy实列
const obj = {
'name': 'kongzhi'
};
const handler = {
set: function(obj, prop, value) {
return Reflect.set(obj, prop, value);
}
};
const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 输出: 我是空智
console.log(obj); // 输出: {name: '我是空智'}
当然如果设置该对象的属性是不可写的,那么set方法就不起作用了,如下代码演示:
const obj = {
'name': 'kongzhi'
};
Object.defineProperty(obj, 'name', {
writable: false
});
const handler = {
set: function(obj, prop, value, receiver) {
Reflect.set(obj, prop, value);
}
};
const proxy = new Proxy(obj, handler);
proxy.name = '我是空智';
console.log(proxy.name); // 打印的是 kongzhi
注意:proxy对数组也是可以监听的;如下代码演示,数组中的 push方法监听:
const obj = [{
'name': 'kongzhi'
}];
const handler = {
set: function(obj, prop, value) {
return Reflect.set(obj, prop, value);
}
};
const proxy = new Proxy(obj, handler);
proxy.push({'name': 'kongzhi222'});
proxy.forEach(function(item) {
console.log(item.name); // 打印出 kongzhi kongzhi222
});
该方法是判断某个目标对象是否有该属性名。接收二个参数,分别为目标对象和属性名。返回的是一个布尔型。
如下代码演示:
const obj = {
'name': 'kongzhi'
};
const handler = {
has: function(target, key) {
if (Reflect.has(target, key)) {
return true;
} else {
return false;
}
}
};
const proxy = new Proxy(obj, handler);
console.log(Reflect.has(obj, 'name')); // true
console.log(Reflect.has(obj, 'age')); // false
construct(target, args, newTarget):
该方法是用来拦截new命令的,它接收三个参数,分别为 目标对象,构造函数的参数对象及创造实列的对象。
第三个参数是可选的。它的作用是拦截对象属性。
如下代码演示:
function A(name) {
this.name = name;
}
const handler = {
construct: function(target, args, newTarget) {
/*
输出: function A(name) {
this.name = name;
}
*/
console.log(target);
// 输出: ['kongzhi', {age: 30}]
console.log(args);
return args
}
};
const Test = new Proxy(A, handler);
const obj = new Test('kongzhi', {age: 30});
console.log(obj); // 输出: ['kongzhi', {age: 30}]
其他转载自
深入理解 ES6中的 Reflect - 龙恩0707 - 博客园 (cnblogs.com)
vue2 和vue3的区别
监听数据原理
vue2的双向数据绑定是利用了es5 的一个API Object.definepropert() 对数据进行劫持 结合发布订阅模式来实现的。vue3中使用了es6的proxyAPI对数据进行处理。
相比与vue2,使用proxy API 优势有:defineProperty只能监听某个属性,不能对全对象进行监听;可以省去for in 、闭包等内容来提升效率(直接绑定整个对象即可);可以监听数组,不用再去单独的对数组做特异性操作,vue3可以检测到数组内部数据的变化。
选项式API和组合式API
optionsAPI
vue2中组织代码:我们会在一个vue文件中data,methods,computed,watch中定义属性和方法,共同处理页面逻辑
上图解释了optionsAPI的缺点,一个功能往往需要在不同的vue配置项中定义属性和方法,比较分散,项目小还好,清晰明了,但项目大了之后,一个methods中可能包含多个方法,往往分不清哪个方法对应着哪个功能,而且当你想要新增一个功能的时候,你可能需要在data,methods,computed,watch中都要写一些东西。但是这个时候选项里面的内容很多你需要上下来回的翻滚,特别影响效率
Composition API
compositionAPI是根据逻辑相关组织代码的,提高可读性和可维护性,基于函数组合的API更好的重用代码逻辑(在vue2 Options API
中通过Mixins
重用逻辑代码,容易发生命名冲突且关系不清)Composition API 最大的优点通俗的讲就是把跟一个功能相关的东西放在一个地方,它是目前最合理也最容易维护的。你可以随时将功能的一部分拆分出去。可以将每一个功能相关所有的东西比如methods、computed都放在如上图的function中,这个function可以独立存在,可以放在一个TS文件中,也可以在NPM独立发布,最终CompositionAPI把他们组合起来
代码
下面的例子,将处理count属性相关的代码放在同一个函数了
function useCount() {
let count = ref(10);
let double = computed(() => {
return count.value * 2;
});
const handleConut = () => {
count.value = count.value * 2;
};
console.log(count);
return {
count,
double,
handleConut,
};
}
组件上使用count
export default defineComponent({
setup() {
const { count, double, handleConut } = useCount();
return {
count,
double,
handleConut
}
},
});
可以很清楚感受到CompositionAPI在逻辑组织方面的优势,以后修改属性功能的时候,只需要调到控制该属性的方法中即可
vue2中,我们是用过mixin去复用相同的逻辑,
export const MoveMixin = {
data() {
return {
x: 0,
y: 0,
};
},
methods: {
handleKeyup(e) {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
this.y--;
break;
case "ArrowDown":
this.y++;
break;
case "ArrowLeft":
this.x--;
break;
case "ArrowRight":
this.x++;
break;
}
},
},
mounted() {
window.addEventListener("keyup", this.handleKeyup);
},
unmounted() {
window.removeEventListener("keyup", this.handleKeyup);
},
};
然后在组建中使用
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>
使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候
mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]
会存在两个非常明显的问题:
- 命名冲突
- 数据来源不清楚 现在通过
Compositon API
这种方式改写上面的代码
import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
const position = reactive({
x: 0,
y: 0,
});
const handleKeyup = (e) => {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
// y.value--;
position.y--;
break;
case "ArrowDown":
// y.value++;
position.y++;
break;
case "ArrowLeft":
// x.value--;
position.x--;
break;
case "ArrowRight":
// x.value++;
position.x++;
break;
}
};
onMounted(() => {
window.addEventListener("keyup", handleKeyup);
});
onUnmounted(() => {
window.removeEventListener("keyup", handleKeyup);
});
return { position };
}
在组件中使用
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
setup() {
const { position } = useMove();
const { x, y } = toRefs(position);
return {
x,
y,
};
},
};
</script>
可以看到,整个数据来源清晰了,即使去编写更多的 hook
函数,也不会出现命名冲突的问题
在逻辑组织和逻辑复用方面,Composition API
是优于Options API
因为Composition API
几乎是函数,会有更好的类型推断。
Composition API
对 tree-shaking
友好,代码也更容易压缩
Composition API
中见不到this
的使用,减少了this
指向不明的情况
如果是小型组件,可以继续使用Options API
,也是十分友好的