下面是从视频课程中学习的总结:www.bilibili.com/video/BV1Zy…
一、Vue2响应式原理
(一)Object.defineProperty(obj, prop, descriptor)
- 参数——obj:要定义属性的对象。 prop:要定义或修改的属性的名称或 Symbol 。 descriptor:要定义或修改的属性描述符。
- 返回值 ——被传递给函数的对象。
通过这种方式定义的属性有以下优点:
1、更精细的控制定义的对象的属性
对象字面量定义的属性,enumerable、writable、configurable属性默认都是true。
var obj={
name:"rectangle",
width:10,
}
Object.defineProperty(obj,'height',{
value:20
})
//可枚举enumerable
for (let key in obj){
console.log("@",key) //width被打印出,height没有打印出
}
//可修改writable
obj.width=20; //width被修改,height修改失败
//可删除configurable
delete obj.width; //width被删除,height删除失败
通过Object.defineProperty()定义的属性,默认情况下,enumerable、writable、configurable都是为false,并且可以改变其取值。按以下方式定义的才和字面量定义的属性一样可遍历、修改、删除。
Object.defineProperty(obj,'height',{
value:20,
enumerable:true,
writable:true,
configurable:true
})
2、配置属性的getter和setter,使得属性可以与特定的变量绑定。
-
getter:当访问属性时,会调用此函数。这个方法返回一个值,这个值就是访问属性获得的值。 -
setter:当属性值被修改时,会调用此函数。这个方法有一个参数,这个参数就是属性修改后的值。let number=30; var obj={ name:"rectangle", width:10, } //height属性和number绑定,number修改时height修改,height修改时number修改 Object.defineProperty(obj,'height',{ get:function(){ console.log("height属性被读取"); return number; }, set:function(value){ console.log("height属性被设置"); number=value } }) obj.height; //输出30 obj.height=22; //obj.height和number都变成22
(二)什么是数据代理
数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)。
下面的代码就是obj2代理obj1的x属性操作的例子。
let obj={x:100};
let obj2={y:200};
//通过obj2代理obj1的x属性的操作
Object.defineProperty(obj2,'x',{
get:function(){
return obj.x;
},
set:function(value){
obj.x=value
}
})
(三)Vue2的数据代理
Vue的数据代理:通过Vue实例对象(vm)来代理data对象(Vue2:_data属性,Vue3:$data属性)中的属性的操作(读/写)。数据代理的目的是为了存取数据方便。
1、通过Object.defineProperty()把data对象中所有属性添加到vm上;
2、为每一个添加到vm上的属性,都指定一个getter/setter;。
3、在getter/setter内部区操作(读/写)data中对应的属性。
二、Vue2数据监视(响应式原理)
研究data中的属性变化,页面中的内容也发生变化的内容。
(一)如何监视Vue中的数据变化
通过Object.defineProperty()对属性的读取、修改进行拦截(getter和setter方法)。Vue通过setter监视data中所有层次的对象的属性,也就是无论对象是作为数组的元素,还是作为对象的属性,只要是对象其属性都会被监视。下面是一个简单的实现对数据监视的例子。
let data={
name:"Alice",
age:20
}
//创建观察者类,接收一个类并对这个类的属性读取进行代理
function Observer(obj){
for(let key in obj){
Object.defineProperty(this,key,{
get(){
return obj[key]
},
set(val){
obj[key]=val
}
})
}
}
const obs=new Observer(data);
console.log(obs);
let vm={};
vm._data=data=obs;
但是Object.defineProperty()对数据拦截,下面两种场景拦截不到。
- 存在新增属性、删除属性;
- 直接通过下标修改数组,界面不会自动更新;
(二)怎么给对象新增响应式属性(被监视到)?
在Vue实例创建后,直接给data中的对象追加属性,是无法进行响应式处理的(没有setter方法)。想要后添加的属性做响应式,需要使用:Vue.set(target,propertyName,value) 或者
vm.$set(target,propertyName,value)
const vm = new Vue({
el: '#app',
data(){
return {
student:{
name:'Alice',
age:20
}
}
},
methods:{
//new Vue()创建Vue实例之后,data中的对象直接添加属性是不显示在页面的
addSignature(){
this.student.signature="I like summer"
},
//new Vue()创建Vue实例之后,通过Vue.set()方法可以给data中的对象添加响应式属性,在页面显示
addDesc(){
Vue.set(this.student,"desc","I like winter")
}
},
template:`
<div>
{{student.name}}-{{student.age}}
<div v-if="student.signature">{{student.signature}}</div>
<div><button @click="addSignature">无效添加属性</button></div>
<div><button @click="addDesc">有效添加属性</button></div>
</div>
`
})
(三)如果对象的属性是数组,如何修改数组才能被监视到?
在Vue修改数组要想被监视到必须要用以下方法:
- 使用改变数组的API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()。本质上Vue调用原生数组对应的方法对数组进行更新,重新解析模板,更新页面。
- Vue.set(target,index,value)或vm.$set(target,index,value)方法
我们知道,数组作为对象的属性是被监视到的,但是数组值的每个item,是不会单独添setter进行监视的。
const vm = new Vue({
el: '#app',
data(){
return {
student:{
friends:[
{id:0,name:"Cindy"},
{id:1,name:"Mike"}
],
hobby:['Singing','Swiming'],
}
}
}
})
控制台输入vm.student,输出的内容如下:
可以看到friends和hobby作为data数据对象属性是被监视的(setter方法),但是hobby[0]或hobby[1]被没有被监视,friends[0]和friends[1]也没有没监视,但是friends[0]/friends[1]是对象,其里面的属性id和name是被监视的。这意味着
friends[0]={id:2,name:"May"} //这个方式修改数组是不被承认的
friends[0].name="John" //这个修改数组是被承认的
(四)Vue2响应式原理总结
对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持) ;
数组类型:通过重写数组的一些列方法进行拦截(对数组的变更方法进行了包裹)。
存在问题:
- 新增属性、删除属性,界面不会自动更新。 解决方案:Vue.set()和Vue.delete()方法 。
- 直接通过下标修改数组,界面不会自动更新。 解决方案:Vue.set()或者改变数组的方法。
这两个问题在Vue3都不存在了。本质上是因为Vue3使用Proxy对象取代Object.defineProperty()进行数据拦截。
三、Vue3数据监视(响应式原理)
(一)Proxy构造函数
创建一个对象代理,拦截对象任何属性变化,包括属性的读写、添加、删除等。
const p = new Proxy(target, handler)
- target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。(代理对象)
- handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
官方文档:developer.mozilla.org/zh-CN/docs/…
使用Proxy对对象进行代理的例子如下,可以对代理对象的属性进行增删改查,还可以通过数组下标修改数组元素(p.side[0]=9成功改变数组)。
let rect={ name:"rectangle", width:10, side:[10,20]}
/*p代理react,即使第二个参数handler只是一个空对象,也能对rect进行增删改查,而
给handler添加get、set、deletePropert的方法,则可以拦截到对rect属性的增删改查,并改变页面。(响应式)
*/
let p=new Proxy(rect,{
//访问属性时(p.width)调用
get(target,property){
console.log(`读取了p身上的${property}属性`)
return rect[property];
},
//修改和新增属性时(p.width=11或p.height=11)都调用,p和rect都变了
set(target,property,value){
console.log(`修改了p身上的${property}属性`)
rect[property]=value
},
//删除属性时调用(delte p.width),p和react都变了
deleteProperty(target,property){
console.log(`删除了p身上的${property}属性`)
return delete target[property];
}
})
对比Vue2数据拦截,如下所示,直接删除对象属性或添加对象属性是不生效的。即便配置上configurable:true,删除的也只是p身上的,rect身上的没有变。
let rect={
name:"rectangle",
width:10,
}
let p={};//每个属性都要用Object.defineProperty定义
Object.defineProperty(p,"width",{
//访问属性时(p.width)调用
get(){
console.log(`读取了p身上的width属性`)
return rect.width;
},
//修改属性时(p.width=11或p.height=11)都调用
set(value){
console.log(`修改了p身上的width属性`)
rect.width=value;
},
})
(二)Reflect内置对象
内置对象,提供拦截Object属于语言内部的方法,这些方法与proxy handlers的方法相同。
obj={a:1,b:2}
Reflect.get(obj,"a") //返回1
Reflect.set(obj,"a",11) //修改a属性值成功
Reflect.deleteProperty(obj,"b") //删除b属性成功
结合使用Reflect和Proxy对对象进行代理,代码如下
let p=new Proxy(rect,{
//访问属性时(p.width)调用
get(target,property){
console.log(`读取了p身上的${property}属性`);
return Reflect.get(rect,property);
//return rect[property];
},
//修改和新增属性时(p.width=11或p.height=11)都调用,p和rect都变了
set(target,property,value){
console.log(`修改或新增了p身上的${property}属性`);
Reflect.set(rect,property,value);
//rect[property]=value
},
//删除属性时调用(delte p.width),p和react都变了
deleteProperty(target,property){
console.log(`删除了p身上的${property}属性`)
//return delete target[property];
return Reflect.deleteProperty(target,property)
}
})