数据劫持(响应式原理)


Vue的响应式原理是使用数据劫持
https://blog.csdn.net/weixin_44019523/article/details/113574552

Vue2是Object.defineProperty(ES5)

Vue3是Proxy对象(ES6)

Object.defineProperty()

本章将介绍Vue2使用的Object.defineProperty(obj,prop,description)接收三个3个参数

使用介绍

我们把重点集中在第三个参数,description是属性描述符

属性描述符的拥有两种表现形式:数据描述符存取描述符。这两种描述符不能同时存在。

如果一个描述符不具有 value、writable、get 和 set 中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。 –MDN

  1. 两种描述符均有以下键值:

(1) configurable :配置。可以修改属性描述符,且属性能被删除,默认false\

let student = {};
Object.defineProperty(student, "sex", {
    //默认false
    //configurable: false,
});

//没有办法在进行配置,以下都无效
Object.defineProperty(student, "sex", {
    value:12,
    configurable:true
});

(2) enumerable :枚举。是否读取该属性访问for…in,默认false

let student = {};
Object.defineProperty(student, "job", {
    enumerable: false,
})

//for...in 是没办法访问的,student.job却可以
  1. 数据描述符可选键值

value : 该属性的初始值。可以是任何(数值,对象,函数等)默认undefined。

writable : 当且进档该属性的writable为true时,该属性才能被写入值。默认为false

let student = {};
Object.defineProperty(student, "name", {
    //设置值
    value: '小明',
    //是否可改写
    writable:true,

    //配置
    configurable:true,
    //枚举
    enumerable: true,
})
  1. 存取描述符可选键值

get : 一个给属性提供getter的方法。如果没有getter则为undefined。当访问属性时,该方法会被执行。会传入this,但由于继承关系,this并不一定是定义改属性的对象。默认为undefined

set : 一个给属性提供setter的方法。如果没有setter则为undefined。当属性被修改时,该方法会被执行。该方法将接受唯一参数。默认为undefined

注意不能在getset方法内写对象属性,会造成栈溢出

let student = {};

//不可以直接在get方法和set方法内使用student.name输出和写入,因为会被检测到 要使用val代存 
let val = student.name;
Object.defineProperty(student, "name", {
    
    get(){
        console.log('拦截到获取');
        return val
    },
    set(newVal){
        console.log('拦截到写入');
        //val自然不等于student.name,但是在get方法上输出的是val呀 没有影响
        val = newVal;
    }

    configurable:true,
    enumerable: true,
})

//也可以这么写,使用形参
let student = {};
(function(val){
    Object.defineProperty(student, "name", {
    get(){
        console.log('拦截到获取');
        return val
    },
    set(newVal){
        console.log('拦截到写入');
        val = newVal;
    },
    configurable:true,
    enumerable: true,
})
})(student.name)

vue2数据劫持

放上清晰明了的代码

/**
 * 数据劫持
 * 监听this属性的修改和获取,访问this.$data
 */
proxyData() {
    for (let key in this.$data) {
        Object.defineProperty(this, key, {
            get() {
                return this.$data[key];
            },
            set(newVal) {
                console.log('监听到Vue实例对象的写入');
                this.$data[key] = newVal;
            }
        })
    }

};

这也暴露了defineProperty导致Vue2必须通过Vue.set方法来进行响应式属性的添加

const obj = {
  name: "_island",
  age: 18
};

const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function (target, key) {
    console.log(`监听到了${key}被获取值`);
    return target[key];
  },
  // 设置值时的捕获器
  set: function (target, key, newValue) {
    console.log(`监听到了${key}被设置值`);
    target[key] = newValue;
  }
});

console.log(objProxy.name);
// 监听到了name被获取值
// _island
console.log(objProxy.age);
// 监听到了age被获取值
// 18
objProxy.name = "QC2125";
// 监听到了name被设置值
console.log(objProxy.name);
// 监听到了name被获取值
// QC2125

Proxy

Proxy对象用于创建一个对象的代理,是用于监听一个对象的相关操作。代理对象可以监听我们对原对象的操作。Proxy对象需要传入两个参数,分别是需要被Proxy代理的对象和一系列的捕获器。

使用介绍

什么是proxy

const obj={
  name:'_island'
}

const objProxy=new Proxy(obj,{});

console.log(objProxy);

打印出来可以看到的是一个Proxy对象。下面我们开始看看Proxy中的捕获器对象。

那么在Proxy也有和defineProperty中 属性描述符 非常相似的参数,就是 捕获器。在实例化Proxy对象时,第二个参数传入的是捕获器集合,我们在其对象内定义一个get捕获器,用于监听获取对象值的操作。

// 定义一个普通的对象obj
const obj = {
  name: "_island"
};

// 代理obj这个对象,并传入get捕获器
const objProxy = new Proxy(obj, {
  // get捕获器
  get: function (target, key) {
    console.log(`捕获到对象获取${key}属性的值操作`);
    return target[key];
  },
});

// 通过代理对象操作obj对象
console.log(objProxy.name);
// 捕获到对象获取name属性的值操作
// _island

在objProxy对象的拦截器中新增一个捕获器set,用于监听对象的某个属性被设置时触发。

// set捕获器
set: function (target, key, val) {
  console.log(`捕获到对象设置${key}属性的值操作,新值为${val}`);
  target[key] = val;
}

console.log(objProxy.name = "QC2125");
// 捕获到对象设置name属性的值操作,新值为QC2125
console.log(objProxy.name);
// 捕获到对象获取name属性的值操作
// QC2125

我们可以定义一些行为

set: function (target, key, val) {
  if (key==='age' && typeof val === "number") {
    target[key] = val;
  } else {
    throw new TypeError("该属性的值必须是Number类型");
  }
}
对象中的方法 对应触发条件
handler.getPrototypeOf() Object.getPrototypeOf 方法的捕捉器
handler.setPrototypeOf() Object.setPrototypeOf 方法的捕捉器
handler.isExtensible() Object.isExtensible 方法的捕捉器
handler.preventExtensions() Object.preventExtensions 方法的捕捉器
handler.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.defineProperty() Object.defineProperty 方法的捕捉器
handler.handler.has() in 操作符的捕捉器
handler.get() 属性读取操作的捕捉器
handler.set() 属性设置操作的捕捉器
handler.deleteProperty() delete 操作符的捕捉器
handler.ownKeys() Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
handler.apply() 函数被apply调用操作的捕捉器
handler.construct() new 操作符的捕捉器

this指向的问题

代理目标对象内部的this会自动改变为Proxy代理对象。

const obj={
 name:'_island',
 foo:function(){
   return this === objProxy
 }
}

const objProxy=new Proxy(obj,{})
console.log(obj.foo()); // false
console.log(objProxy.foo()); // true

vue3中的proxy

/**
 * 数据劫持
 * 监听this属性的修改和获取,访问this.$data
 */
proxyData() {
	return new Proxy(this.$data, {
		get(target, propKey, receiver) {
			console.log('你访问了' + propKey);
			return Reflect.get(target, propKey, receiver);
		},
		set(target, propKey, value, receiver) {
			console.log('你设置了' + propKey);
			console.log('新的' + propKey + '=' + value);
			Reflect.set(target, propKey, value, receiver);//一个对象上设置一个属性。
		}
	});
};

总结

关于数据劫持,Object.defineProperty只能监听到具体的对象属性,而proxy是一个的代理对象,它可以代理我们对原目标的操作,也就是能监听到整个对象,这也就是为什么vue3之后不再需要Vue.$set方法了。


文章作者: iamfugui
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 iamfugui !
评论
  目录