使用computed配合Proxy拦截v-model

前言

在写表单组件时常常会遇到这种情况,父组件将一个对象交给子组件,子组件要对对象的值进行编辑。
我们常见的做法有这么几种

  1. 使用computedget获取值,set触发emit。这样是可以的,但是对象的属性多的话,需要写许多的computed一一对应,麻烦。
  2. 什么都不管,直接就是v-model对象的属性。这种打破了单向数据流,本质的子组件修改了值。
  3. 使用watch,这个跟第一种computed差不多,也是麻烦,而且更容易乱。

我觉得大部分人都是用这3种的一种,前一段时间在刷短视频的时候,发现了一个很厉害的写法(有人的应该刷到过),使用computedProxy来巧妙的处理,只能说还是水平不够。

正文

在这之前还是要了解一下什么是Proxy,有些人可能只在背面试题中背过。

什么是Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法

const p = new Proxy(target, handler)

target  要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler   一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。

handler 对象的方法

下面例举了一部分的方法,具体的可以看Proxy – JavaScript | MDN (mozilla.org),但是我们会用上的基本只用getset

let obj = { name: 'John' };
let newObj = { age: 10 };
let proxy = new Proxy(obj, {
  // 用于拦截对对象的 Object.getPrototypeOf() 操作
  getPrototypeOf(target) {
    console.log('getPrototypeOf 被调用');
    return Reflect.getPrototypeOf(target);
  },
  //用于拦截对对象的 Object.setPrototypeOf() 操作
  setPrototypeOf(target, proto) {
    console.log('setPrototypeOf 被调用');
    return Reflect.setPrototypeOf(target, proto);
  },
  //Object.isExtensible 方法的捕捉器。
  isExtensible: function (target) {
    console.log("Called isExtensible on proxy");
    return Object.isExtensible(target);
  },
  //Object.preventExtensions 方法的捕捉器。
  preventExtensions: function (target) {
    console.log("Called preventExtensions on proxy");
    Object.preventExtensions(target);
    return true;
  },
  //Object.getOwnPropertyDescriptor 方法的捕捉器。
  getOwnPropertyDescriptor: function (target, prop) {
    console.log(`Called getOwnPropertyDescriptor on ${prop}`);
    return Object.getOwnPropertyDescriptor(target, prop);
  },
  //Object.defineProperty 方法的捕捉器。
  defineProperty: function (target, property, descriptor) {
    console.log(`定义属性 ${property}`);
    return Reflect.defineProperty(target, property, descriptor);
  },
  //in 操作符的捕捉器
  has(target, prop) {
    console.log(`检查 ${prop}`);
    return Reflect.has(target, prop);
  },
  //属性读取操作的捕捉器
  get(target, prop, receiver) {
    console.log(`读取 ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  //属性设置操作的捕捉器。
  set(target, prop, value, receiver) {
    console.log(`设置 ${prop}`);
    return Reflect.set(target, prop, value, receiver);
  },
  //delete 操作符的捕捉器
  deleteProperty(target, prop) {
    if (prop in target) {
      delete target[prop];
      console.log(`Property ${prop} has been deleted.`);
      return true;
    } else {
      console.log(`Property ${prop} does not exist.`);
      return false;
    }
  },
  //Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
  ownKeys(target) {
    return Reflect.ownKeys(target).filter(key => typeof key !== 'symbol');
  },
});

深度监听对象

let obj = {
  name: 'aa',
  age: 19,
  children: {
    childrenName: 'bb',
    childrenAge: 20
  }
}

const setProxyObj = (obj) => {
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  return new Proxy(obj, {
    get(target, key) {
      console.log('get', key)
      if (typeof target[key] === 'object') {
        return setProxyObj(target[key])
      }
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      console.log('set', key)
      return Reflect.set(target, key, value)
    }
  })
}
let proxy = setProxyObj(obj)

proxy.age = {
  num: 12,
}
proxy.age.num = 14

//输出
// set age
// get age
// set num

什么是Object.defineProperty

Object.defineProperty()  静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。

语法

Object.defineProperty(obj, prop, descriptor)

obj  要定义属性的对象。
prop 一个字符串或Symbol,指定了要定义或修改的属性键。
descriptor 要定义或修改的属性的描述符。

例子

const object1 = {};
Object.defineProperty(object1, 'property1', {
  value: 42,
});
console.log(object1.property1);

对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是一个具有可写或不可写值的属性。访问器描述符是由 getter/setter 函数对描述的属性。

描述符只能是这两种类型之一,不能同时为两者。也就是说,一个属性不能同时有 value 或 writable 和 get 或 set 键。

数据描述符

  1. value

与属性相关联的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。默认值为 undefined。

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
});
console.log(object1.property1);  // 输出: 42
  1. writable

属性值可以使用赋值运算符更改。默认值为 false

const object1 = {};
Object.defineProperty(object1, 'property1', {
  value: 42,
});
object1.property1 = 77;
console.log(object1.property1);  // 输出: 42

//----------------------------------------------------------
const object1 = {};
Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: true
});
object1.property1 = 77;
console.log(object1.property1);  // 输出: 77
  1. enumerable(与访问器描述符共享可选键)

属性可以在循环中被枚举。默认值为 false

const object1 = {};
Object.defineProperty(object1, 'property1', {
  value: 42,
  enumerable: false,
});
console.log(Object.keys(object1)); //[]

//----------------------------
const object1 = {};
Object.defineProperty(object1, 'property1', {
  value: 42,
  enumerable: true,
});
console.log(Object.keys(object1)); //[ 'property1' ]
  1. configurable(与访问器描述符共享可选键)

 属性设置为 true 或 false

  • 如果 configurable 设置为 true,那么这个属性描述符的类型可以被改变,也可以从对应的对象上被删除。
  • 如果 configurable 设置为 false,那么这个属性描述符的类型不可以被改变,也不可以从对应的对象上被删除。

“这个属性描述符的类型可以被改变”的意思是,如果一个属性的configurable设置为true,那么你可以再次使用Object.defineProperty()来修改这个属性的描述符。

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  configurable: true
});
Object.defineProperty(object1, 'property1', {
  value: 29,
});

console.log(object1.property1);  // 输出: 29
delete object1.property1;  // 删除属性
console.log(object1.property1);  // 输出: undefined

访问器描述符

  1. get

用作属性 getter 的函数,如果没有 getter 则为 undefined。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined

  1. set

用作属性 setter 的函数,如果没有 setter 则为 undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象。默认值为 undefined

例子

let obj = {
  _name: 'default' // 私有变量
};
Object.defineProperty(obj, 'name', {
  get: function() {
    return this._name;
  },
  set: function(value) {
    this._name = value;
  }
});
console.log(obj.name); // 输出: 'default'
obj.name = 'new name'; // 使用 setter
console.log(obj.name); // 输出: 'new name'

深度监听对象

let obj = {
  name: 'aa',
  age: 19,
  children: {
    childrenName: 'bb',
    childrenAge: 20
  }
}

const setDefinePropertyObj = (obj) => {
  if (typeof obj !== 'object' || obj === null) {
    return
  }
  Object.keys(obj).forEach((key) => {
    let value = obj[key]
    if (typeof value === 'object') {
      setDefinePropertyObj(value)
    }
    Object.defineProperty(obj, key, {
      get() {
        console.log('get', key)
        return value
      },
      set(newValue) {
        console.log('set', key)
        if (typeof newValue === 'object') {
          setDefinePropertyObj(newValue)
        }
        value = newValue
      }
    })
  })
}
setDefinePropertyObj(obj)

Proxy实战使用

前面讲了那么多,只是为了让大家了解知识,真正还是要看知识在实际中的运用(使用computed配合Proxy拦截v-model),不然就是光说不做假把戏。

父组件

<template>
  <div id="app">
    <Test v-model="form.user" />
  </div>
</template>

<script lang="ts">
import Test from './pages/test/index.vue'
export default {
  components: {
    Test,
  },
  props: {
  },
  data() {
    return {
      form: {
        user: {
          name: "aaaa",
          age: 12,
        }
      }
    }
  },
  mounted() {
  },
  methods: {
  }
}
</script>
<style  scoped></style>

子组件

<template>
  <div>
    <el-input v-model="model.name" placeholder="请输入内容"></el-input>
    <el-input v-model="model.age" placeholder="请输入内容"></el-input>
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: Object,
      default: () => { }
    }
  },
  data() {
    return {
    }
  },
  computed: {
    model: {
      get() {
        let that = this
        const proxy = new Proxy(this.value, {
          get(target, key) {
            return Reflect.get(target, key)
          },
          set(target, key, value) {
            console.log('set', key, value);
            that.$emit('input', { ...that.value, [key]: value });
            return true
          }
        })
        return proxy
      },
      set(val) {
        this.$emit('input', val)
      }
    }
  },
  methods: {
  }
}
</script>
<style  scoped></style>

结语

感兴趣的可以去试试

原文链接:https://juejin.cn/post/7338634091397431330 作者:卸任

(0)
上一篇 2024年2月24日 下午4:32
下一篇 2024年2月24日 下午4:42

相关推荐

发表回复

登录后才能评论