likes
comments
collection
share

ES6-defineProperty和proxy(数据拦截即响应式的实现)

作者站长头像
站长
· 阅读数 33

前言

大家都知道在 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()来获取和设置该被劫持属性的值,那么不能使用操作属性的valuewritable属性,会冲突。当要访问被劫持属性的值,那么会触发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);

ES6-defineProperty和proxy(数据拦截即响应式的实现)

响应式应用

在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}`);
}

ES6-defineProperty和proxy(数据拦截即响应式的实现)

实际应用

<!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>

ES6-defineProperty和proxy(数据拦截即响应式的实现)

proxy

proxy是ES6推出的方法,也叫数据代理,其完善了defineProperty的不足,因为defineProperty只能重定义属性的读取和设置,不能拦截删除等操作,而proxy可以重定义更多行为,总共支持包括get、set等13种行为。(大家也可以去查看更详细介绍:Proxy - ECMAScript 6入门 (ruanyifeng.com)

ES6-defineProperty和proxy(数据拦截即响应式的实现)

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);

ES6-defineProperty和proxy(数据拦截即响应式的实现)

至此,它和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); //直接访问

ES6-defineProperty和proxy(数据拦截即响应式的实现)

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);

ES6-defineProperty和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>

ES6-defineProperty和proxy(数据拦截即响应式的实现)

总结

以上就是我总结的数据拦截方法,它是vue中实现响应式的基础,但是真正要实现vue中的响应式,还需要很多其他部分,结合在一起,希望对大家有所帮助

转载自:https://juejin.cn/post/7268820544995901495
评论
请登录