ES6-defineProperty和proxy(数据拦截即响应式的实现)
前言
大家都知道在 Vue 中很重要的一部分就是响应式变量,就是能感知到变量的变化并做出响应。要实现这样的效果,那么就要对变量进行代理,也就是监听,当数据要变化时需要通过这个代理来实现,这样代理就能感知到变量数据的改变,也就实现了响应式。在ES5中,官方为对象提供了一个api就是defineproperty
,也叫数据劫持,早期Vue2也是通过这个来实现数据响应式的,但存在一些不足,之后ES6就推出 proxy
增加了许多功能来完善不足,也叫数据代理,所以之后尤雨溪就在Vue3修改了响应式的部分源码,就是使用的proxy
。下面我也总结了这两个api的具体使用,和区别,帮助大家理解。
defineproperty
defineproperty
是使用于对象的一个方法,在ES5中被提供出来,也叫数据劫持方法,可以在一个对象定义一个新的属性,或修改对象现有的属性,使用语法是Object.defineProperty(obj,prop,desc)
,其中可以接收三个参数,第一个参数是被劫持的属性(obj),第二个参数指要定义或者修改的属性(prop),第三个参数是描述被定义或者修改的属性(desc)。其中第三个参数主要包含四个操作属性和两个存取器属性:
Object.defineProperty(obj,prop,{
value: xxxx, //设置新属性的值或者修改原有属性的值
writable: true/false, //是否可写,即是否可以修改该属性的值
enumerable: true/false, //是否可枚举(遍历),即该属性是否可以被遍历到
configurable:true/false, //是否可配置,即该属性是否可以被删除等
get(){ //获取该属性的值时触发
},
set(){ //设置属性的值时触发
}
})
四个操作属性:
value
可以设置新属性的值或者修改原有属性的值
let obj = {
num:1
}
Object.defineProperty(obj,'count',{
value:2,
writable:true,
enumerable:true,
configurable:true,
})
console.log(obj) //{ num: 1, count: 2 }
writable
若设置writable
的值为false,那么该属性被劫持后,其值无法被修改
let obj = {
num:1
}
Object.defineProperty(obj,'num',{
value:2,
writable:false,
enumerable:true,
configurable:true,
})
obj.num = 3
console.log(obj) //{ num: 2 }
enumerable
若设置enumerable
的值为false,那么该属性被劫持后无法被遍历到,但是可以直接访问该属性
let obj = {
num:1
}
Object.defineProperty(obj,'count',{
value:2,
writable:true,
enumerable:false,
configurable:true,
})
console.log(obj.count); //2
for(let key in obj) {
console.log(key); //num
}
configurable
若设置configurable
的值为false,那么该属性被劫持后不能被做删除操作等
let obj = {
num:1
}
Object.defineProperty(obj,'count',{
value:2,
writable:true,
enumerable:true,
configurable:false,
})
delete obj.count
console.log(obj); //{ num: 1, count: 2 }
存取器属性
get()和set()
当使用get()和set()
来获取和设置该被劫持属性的值,那么不能使用操作属性的value
和writable
属性,会冲突。当要访问被劫持属性的值,那么会触发get(),并得到的是get()中返回的值;当要修改被劫持属性的值就会触发set()
,其天生具有一个参数newVal,表示新的值
var obj = {},value = null //value用于承接num的值,如果直接在get()使用obj.num,那么会陷入死循环
Object.defineProperty(obj,'num',{
get(){
console.log('执行了 get 操作');
return value
},
set(newVal){
console.log('执行了 set 操作');
value = newVal
}
})
obj.num = 1
console.log(obj.num);
响应式应用
在Vue中我们可以使用reactive、ref等定义响应式变量,实现他们,就可以使用到defineProperty
,我们需要在set()中去操作改变DOM结构,那么就实现当变量发生改变,页面DOM也发生改变,即响应式。这里我们并没有操作DOM结构,只是为了代指触发了操作函数
function Reactive(){
var value = null
var reactive = []
Object.defineProperty(this,'num',{
get(){
console.log('执行了 get 操作');
return value
},
set(newVal){
console.log('执行了 set 操作');
value = newVal
reactive.push({val:newVal})
fn(newVal)
}
})
this.getReactive = function(){ return reactive }
}
var state = new Reactive()
state.num = 11
state.num = 22
console.log(state.getReactive());
function fn(n){
console.log(`val值改为:${n}`);
}
实际应用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="content">1</p>
<button id="btn">+</button>
<script>
let btn = document.getElementById("btn");
let p = document.getElementById("content");
let obj ={
value:1
}
let value = 1 //防止进入死循环
Object.defineProperty(obj,'value',{
get(){
return value
},
set(newVal){
value = newVal
p.innerHTML = newVal //这里固定p标签只为理解,真正封装时,会使用依赖收集来感知需要修改的DOM结构,较复杂,不做解释
}
})
btn.addEventListener("click",() => {
obj.value += 1
})
</script>
</body>
</html>
proxy
proxy
是ES6推出的方法,也叫数据代理,其完善了defineProperty
的不足,因为defineProperty
只能重定义属性的读取和设置,不能拦截删除等操作,而proxy
可以重定义更多行为,总共支持包括get、set等13种行为。(大家也可以去查看更详细介绍:Proxy - ECMAScript 6入门 (ruanyifeng.com))
proxy
代理一个对象,会创建一个新的对象等于这个被代理对象,所以不需要像defineProperty
一样使用额外变量防止死循环。使用语法是var proxy = new Proxy(target,hander)
,其中第一个参数是被代理的对象,第二个参数是一个对象,包含被代理对象的行为。
var proxy = new Proxy({},{ //这里代理一个空对象,得到一个新的对象proxy
get(obj,prop){ //形参:obj代表代理对象 ,prop代理对象中的属性
console.log('get 操作');
return obj[prop]
},
set(obj,prop,value){ //形参:obj代表代理对象 ,prop代理对象中的属性,value设置的新的值
console.log('set 操作');
obj[prop] = value
}
})
proxy.age = 18
console.log(proxy.age);
至此,它和defineProperty
都可拦截get和set行为,但是proxy
可以拦截更多行为。这里只举has()
和deleteProperty()
has()
has(target, propKey) :拦截propKey in proxy
的操作,返回一个布尔值。这是一个判定操作,判定一个属性是否在被代理对象中,也可以在has()中操作来改变判定结果,会影响for...in
的遍历
var target = {
name:'jx',
age:18,
_weight:100
}
var proxy = new Proxy(target,{
get(obj,prop){
console.log('get 操作');
return obj[prop]
},
set(obj,prop,value){
console.log('set 操作');
obj[prop] = value
},
has(obj,prop){
if(prop[0] === '_'){ //将以'_'开头的属性判定为不存在,但是仍可以直接访问
return false
}
return prop in obj
}
})
console.log('name' in proxy);
console.log('_weight' in proxy);
console.log(proxy._weight); //直接访问
deleteProperty()
deleteProperty(target, propKey) :拦截delete proxy[propKey]
的操作,返回一个布尔值。当要删除被代理对象的属性时,会触发该行为函数。
var target = {
name:'jx',
age:18,
_weight:100
}
var proxy = new Proxy(target,{
get(obj,prop){
console.log('get 操作');
return obj[prop]
},
set(obj,prop,value){
console.log('set 操作');
obj[prop] = value
},
has(obj,prop){
if(prop[0] === '_'){
return false
}
return prop in obj
},
deleteProperty(obj,prop){
console.log(`属性${prop}被移出`); //这里并没有真正被删除,只表示触发了该行为函数
}
})
delete proxy.age
console.log(proxy);
实际应用
实现vue中watch的监听功能:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="content">1</p>
<button id="btn">点击+1</button>
<script>
function watch(target, handle) {
var proxy = new Proxy(target, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
handle(value) //当被代理属性值改变即触发handle
}
})
return proxy
}
let btn = document.getElementById('btn')
let p = document.getElementById('content')
var obj = { value: 1 }
var proxyObj = watch(
obj,
(newVal) => { //即触发的handle
p.innerHTML = newVal
}
)
btn.addEventListener('click', () => {
proxyObj.value += 1
})
</script>
</body>
</html>
总结
以上就是我总结的数据拦截方法,它是vue中实现响应式的基础,但是真正要实现vue中的响应式,还需要很多其他部分,结合在一起,希望对大家有所帮助
转载自:https://juejin.cn/post/7268820544995901495