JavaScript-Proxy & 数据劫持
引:
作为一名参与前端业务的开发者,在了解框架的同时也应了解框架背后的原理
在Vue、React等框架的使用中,双向绑定是最常使用的API之一,其基本思想是使用JS实现数据劫持
数据劫持:
简单来说就是在数据改变时对数据进行监听(处理)
其中Proxy是数据劫持的一种方案
Proxy:
我们可以按照如下方式定义一个 Proxy
const testProxy = new Proxy(target, handler);
target:
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler:
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 testProxy 的行为
当外界每次对obj进⾏操作时,就会执⾏handler对象的⽅法
handler中常⽤的对象⽅法如下:
- get(target, propKey, receiver)
- set(target, propKey, value, receiver)
- has(target, propKey)
- construct(target, args):
- apply(target, object, args)
接下来上一个简单的操作用例:
// Proxy是es6新增的,不支持旧版IE
const carInfo = {
brand: '梅赛德斯',
model: 'c500',
price: 5000000,
handlePrice: 0
}
function editPrice(dom){
testProxy.price = dom.value;
}
function handlePrice(dom){
testProxy.handlePrice = dom.value;
}
window.onload = () =>{
document.getElementById('brand').innerHTML = testProxy.brand;
document.getElementById('model').innerHTML = testProxy.model;
document.getElementById('price').innerHTML = testProxy.price;
}
const testProxy = new Proxy(carInfo, {
// 获取对象属性值时触发
// target:原对象,key:对应属性
get: function(target, key) {
console.log(`车辆信息:${key} 被访问`)
return target[key];
},
// 改变/赋值对象属性值时触发
// target:原对象,key:对应属性,newValue:改变的值
set: function(target, key, newValue) {
if (key === "price"){ // 对价格进行拦截校验
if (typeof newValue !== "number") {
alert("价格字段必须为Number类型") // 异常后输入框数值初始化
document.getElementById('price').value = target[key]
return
}else if (newValue < 300000){
alert("价格不能低于300000")
document.getElementById('price').value = target[key]
}else {
target[key] = newValue;
console.log(`车辆信息:${key} 已被更改`)
}
} else if (key === 'handlePrice') {
testProxy.price = target['price'] + parseInt(newValue)
console.log(`价格已被更改 ${testProxy.price}`)
document.getElementById('handlePrice').value = 0;
document.getElementById('price').innerHTML = target['price'];
} else {
target[key] = newValue;
console.log(`车辆信息:${key} 已被更改`)
}
// Proxy仅作为 => 代理 如需要改变原数据,则需要做此赋值操作
// target[key] = newValue
}
})
testProxy.brand = '大众',
testProxy.price = 300001
控制台输出:
据此,我们了解到了Proxy的基本使用,Proxy可以监听数据的get和set操作
根据此特性我们其实可以应用到很多业务场景
get:
1、特殊数据读取、访问埋点
2、特殊数据读取/访问时更改格式 50 => 50m/kg/min…
set:
1、表单校验/拦截
2、双向绑定
3、数据保护
校验demo(实现了一个简易的双向绑定):
// Proxy是es6新增的,不支持旧版IE
const carInfo = {
brand: '梅赛德斯',
model: 'c500',
price: 5000000,
handlePrice: 0,
handlePriceFlag: false
}
function editPrice(dom){
testProxy.price = dom.value;
}
function handlePrice(dom){
testProxy.handlePrice = dom.value;
}
function edit(dom){
testProxy.handlePriceFlag = true;
testProxy.handlePrice = dom.value;
}
window.onload = () =>{
document.getElementById('brand').innerHTML = testProxy.brand;
document.getElementById('model').innerHTML = testProxy.model;
document.getElementById('price').innerHTML = testProxy.price;
}
const testProxy = new Proxy(carInfo, {
// 获取对象属性值时触发
// target:原对象,key:对应属性
get: function(target, key) {
console.log(`车辆信息:${key} 被访问`)
return target[key];
},
// 改变/赋值对象属性值时触发
// target:原对象,key:对应属性,newValue:改变的值
set: function(target, key, newValue) {
if (key === "price"){ // 对价格进行拦截校验
if (typeof newValue !== "number") {
alert("价格字段必须为Number类型") // 异常后输入框数值初始化
document.getElementById('price').value = target[key]
return
}else if (newValue < 300000){
alert("价格不能低于300000")
document.getElementById('price').value = target[key]
}else {
target[key] = newValue;
console.log(`车辆信息:${key} 已被更改`)
}
} else if (key === 'handlePrice') {
console.log(`价格已被更改 ${testProxy.price}`)
document.getElementById('handlePriceValue').innerHTML = newValue;
} else if (key === 'handlePriceFlag' && newValue === true){
testProxy.price = target['price'] + (parseInt(target['handlePrice']) || 0);
console.log('testProxy.price', testProxy.price);
document.getElementById('price').innerHTML = testProxy.price;
document.getElementById('handlePrice').value = 0;
testProxy.handlePriceFlag = false;
} else {
target[key] = newValue;
console.log(`车辆信息:${key} 已被更改`)
}
// Proxy仅作为 => 代理 如需要改变原数据,则需要做此赋值操作
// target[key] = newValue
// 设置新值,保存至全局
const rets = Reflect.set(target, key, newValue)
return rets
}
})
testProxy.brand = '大众';
testProxy.price = 300001;
效果:
Object.defineProperty:
在JS中数据劫持的方案不止只有Proxy,其中 Object.defineProperty 也是一种方案
该方案在Vue2的数据绑定中进行了广泛应用,虽然其有一些局限性不如Proxy灵活(Object.defineProperty操作对象需要递归,略嫌麻烦),但该API对较旧版IE适配,这是一个比较大的优势
这块底层玩意儿我当时写毕业论文的时候深入学习过,所以在此就不过多解释了,偷个懒搬砖搬过来吧(节选)
Vue-数据双向绑定(v-model)
Vue中的双向绑定功能在工程开发中十分常见。所谓双向绑定,即为数据与模型(model)之间的绑定。在最常见的开发中,我们可以在input上通过v-model绑定data中的某个已定义的数据源,此时当我们在input中进行写入删除操作时,data中的绑定的数据将随之input中的值而改变。
双向绑定十分常见并且实用,但我们使用原生js实现此功能却并没有想象中简单。在Vue中v-model是由v-bind+v-on实现的,其中v-bind绑定属性,v-on绑定事件。
数据在v-on与v-bind中相互指向,使用$event.target.value指向当前dom的value属性值。综上,双向绑定可以说仅仅只是一个语法糖。即view更新data,data也更新view。接下来将使用原生写法尝试实现。
首先可以明确,我们要想实现双向绑定,必须要能够监听数据的实时变化。在原生js开发中,使用视图更新数据十分常见且容易,只需要绑定事件监听即可。但关键点应是利用数据的变化而改变视图,我们必须要清楚地了解数据什么时候发生了变化、变化了什么。
在js原生的方法Object.defineProperty( )中有一个set函数,当对象数据发生了变化即会触发。我们可以利用这个特性去监听并且触发函数。再反观.vue文件的构成中,data就是一个对象,我们可以使用递归方法遍历这个对象的属性值,进行Object.defineProperty( )处理。
创建空对象:
方法函数:
控制台输出:
尝试手动改变对象值:
set函数:
控制台结果 :
Vue中的双向绑定功能在工程开发中十分常见。所谓双向绑定,即为数据与模型(model)之间的绑定。在最常见的开发中,我们可以在input上通过v-model绑定data中的某个已定义的数据源,此时当我们在input中进行写入删除操作时,data中的绑定的数据将随之input中的值而改变。
由以上分析可以得出Vue的数据响应式和Object.defineProperty( )是分不开的。即Vue利用其set函数进行数据劫持。
当然,正式的Vue功能并没有完全实现,仍还缺少订阅的问题,正式的功能是由初始化时调用get函数并进行判断操作,最后必须进行解析操作识别指令在DOM中正式绑定。至此双向绑定才算正式完成
总结:
1、框架的原理理解起来并不难,难的原因是自己认为难而不去了解
2、底层的东西应该广泛深入学习,不应该抱有能用就行的心态