vue2
vue2 利用 es5 的 Object.defineProperty 实现响应式
下面详解一下 Object.defineProperty
- 定义
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象 - 语法:
Object.defineProperty(obj, key, descriptor) - 参数说明:
1、obj要在其上定义属性的对象
2、要定义或修改的属性的名称
3、将被定义或修改的属性描述符 - 返回值:
被传递给函数的对象
下面我们通过使用 Object.defineProperty 创建一个对象来熟悉它的用法
正常创建对象这样就可以了
let vm = {
name: '简书'
}
使用 Object.defineProperty 创建对象
let vm = Object.defineProperty({},"name",{
get() {
console.log("执行get");
return name || "简书"
},
set(newVal) {
console.log("执行set");
console.log("新值:" + newVal);
if (newVal !== name) {
name = newVal;
}
}
})
其实这两种都创建一个对象的方式,通过Object.defineProperty创建的对象,我们可以看到,多了上面说的两个存取描述符键值方法get 和set 这样的对象,就变得可控被观察,也就是我们说的被劫持,当我们改变或者获取这对象的属性的时候,我们就可以控制到它
下面我们改变vm.name = "1",我们通过控制台可以看到
实现观察者机制,响应式对象
let vm = {
id:"jianshu",
name:"简书"
};
let keys = Object.keys(vm); //Object.keys() 方法会返回一个数组,数组元素就是vm对象的属性名 ['id', 'name']
keys.forEach(key=>{ //keys.forEach 没有返回结果,返回值为undefined,本质上等同于 for 循环,会改变原数组
let value = vm[key];
Object.defineProperty(vm, key,{
get() {
console.log("执行get");
return value
},
set(newVal){
console.log("执行set");
if(newVal !== value){
value = newVal;
}
}
})
})
vm.id = "test";
console.log(vm)
控制台输出
这里主要是遍历对象中的每一个属性,每个属性都是赋予get和set,让对象中的每一个属性的改变都会被监测到,检测到改变后,vue会触发rerender函数,重新渲染页面,也就是实现了响应式
Object.defineProperty 的缺点
- 无法监听对象非已有的属性的添加和已有属性的删除
只会对对象原有的全部属性进行做数据劫持,也就是说Vue 不允许动态添加或者删除对象已有属性,它是不做数据劫持的,也就不能实现响应式
举例
<template>
<div>
<h1>{{ vm }}</h1>
<button @click="addAttribute">新增属性</button>
<button @click="delAttribute">删除属性</button>
</div>
</template>
<script>
export default {
data() {
return {
vm:{
id:"juejin",
name:"掘金"
}
}
},
methods: {
addAttribute() {
this.vm.use = "codercao"
console.log(this.vm)
},
delAttribute() {
for(let k in this.vm) {
if(k=='id'){
delete this.vm[k]
}
}
console.log(this.vm)
}
},
}
</script>
新增,你会发现控制台打印的vm已经新增了use属性,而页面并没有响应式改变
删除,你会发现控制台打印的vm已经删除了id属性,而页面并没有响应式改变
- 数组对象也不能通过属性或者索引(length,index)实现响应式
vue源码只是对数组的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'进行了重写,但是索引控制数组是没有办法实现响应式的
解决问题
Object.defineProperty 的缺点1可以使用Vue.set(object, propertyName, value) 方法解决,你会发现页面就实现了响应式
addAttribute() {
//this.vm.use = "codercao"
this.$set(this.vm,'use','codercao')
console.log(this.vm)
},
vue3
vue3 利用 ES6 的 proxy 实现响应式
下面详解一下 Object.defineProperty
- 定义
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等) - 通俗解释
假如我们有一个对象 obj,使用 Proxy 对象可以给我们创建一个代理,就好比我们打官司之前,可以先去律师事务所找一个律师,律师全权代理我们。有了这个代理之后,就可以对这个 obj 做一些拦截或自定义,比如对方想要直接找我们谈话时,我们的律师可以先进行拦截,他来判断是否允许和我谈话,然后再做决定。这个律师就是我们对象的代理,有人想要修改 obj 对象,必须先经过律师那一关。 - 语法
let p = new Proxy(target, handler); - 参数
target 是需要被代理的对象,它可以是任何类型的对象,比如数组、函数等等,注意不能是基础数据类型。
我们使用 new 关键词后生成了一个代理对象 p,它就和我们原来的对象 obj 一样,只不过它是一个 Proxy 对象,我们打印出来看看就能更好理解了。
<script>
let obj = {
name: '小猪课堂',
age: 23
}
let p = new Proxy(obj, {});
console.log(obj);
console.log(p);
</script>
上面只是简单的初始化了一个代理对象,而我们需要重点掌握的是 Proxy 对象中的 handler 参数。因为我们所有的拦截操作都是通过这个对象里面的函数而完成的。
就好比律师全权代理了我们,那他拦截之后能做什么呢?或者说律师拦截之后他有哪些能力呢?这就是我们 handler 参数对象的作用了,接下来我们就一一来讲解下。
handler 它是一个对象,该对象的属性通常都是一些函数,handler 对象中的这些函数也就是我们的处理器函数,主要定义我们在代理对象后的拦截或者自定义的行为。
//handler 对象的的属性大概有下面这些
handler.apply()
handler.construct()
handler.defineProperty()
handler.deleteProperty()
handler.get()
handler.getOwnPropertyDescriptor()
handler.getPrototypeOf()
handler.has()
handler.isExtensible()
handler.ownKeys()
handler.preventExtensions()
handler.set()
handler.setPrototypeOf()
这里主要讲解一下 handler.get() 和 handler.set(),其他的 handler属性不在此过多阐述,因为我们是为了讲解Vue响应式数据的原理
handler.get
该方法用于拦截对象的读取属性操作,比如我们要读取某个对象的属性,就可以使用该方法进行拦截。
- 语法
// 拦截读取属性操作
let p5 = new Proxy(target, {
get: function (target, property, receiver) {
//target:被代理的目标对象
//property:想要获取的属性名
//receiver:Proxy 或者继承 Proxy 的对象
}
});
- 案例
// 拦截读取属性操作
let p5 = new Proxy({}, {
get: function (target, property, receiver) {
console.log("属性名:", property); // 属性名:name
console.log(receiver); // Proxy {}
return target[property] || '小猪课堂'
}
});
console.log(p5.name); // 小猪课堂
可以看到我们代理的对象其实是一个空对象,但是我们获取 name 属性是是返回了值的,其实是在 handler 对象中的 get 函数返回的。
handler.set
当我们给对象设置属性值时,将会触发该拦截
- 语法
let p12 = new Proxy(target, {
set: function (target, property, value, receiver) {
//target:被代理的目标对象
//property:将要被设置的属性名
//value:新的属性值
//receiver:最初被调用的对象,通常就是 proxy 对象本身
}
});
- 以下操作会触发拦截:
1.指定属性值:proxy[foo] = bar 和 proxy.foo = bar
2.指定继承者的属性值:Object.create(proxy)[foo] = bar
3.Reflect.set() - 案例
let p12 = new Proxy({}, {
set: function (target, property, value, receiver) {
target[property] = value;
console.log('property set: ' + property + ' = ' + value); // property set: a = 10
return true;
}
});
p12.a = 10;
- 注意
set() 方法应当返回一个布尔值
好了,学习了proxy的原理,我们再看下如下代码的控制台输出
let vm = {
id:"jianshu",
name:"简书"
}
let newVm = new Proxy(vm,{
get(target,key){
console.log("执行get");
return target[key];
},
set(target,key,newVal){
console.log("执行set");
if(target[key]!==newVal)
target[key] = newVal;
}
})
newVm.use = "codercao" //会触发set拦截
console.log(newVm)
你会发现新增的属性也是响应式的,用Proxy 也一样实现了一个简易的观察者机制,当然深入研究的话,你还可以实现双向绑定。
总结
- Object.defineProperty 是监听data里现有的属性,所以data对象有新增的属性是监听不到的
- proxy是监听data这个对象,所以data对象上有新增的属性也会被监听到
网友评论