- 什么是数据劫持
- Object数据劫持实现原理
- Array数据劫持的实现原理
- Proxy、Reflect
一、什么是数据劫持
定义:访问或者修改对象的某个属性时,在访问和修改属性值时,除了执行基本的数据获取和修改操作以外,还基于数据的操作行为,以数据为基础去执行额外的操作。
当前最经典的数据劫持应用就是数据渲染,各大前端框架的核心功能都是基于数据渲染来实现。
数据劫持实现的核心API就是在ES5中提供的Object.defineProperty()以及基于数组的数据修改方法push、pop、unshift、shift、slice、sort、reverse。
为什么要使用数据劫持?
在前端页面渲染中,最经典的触发渲染方案必然是基于事件机制实现,这种实现渲染的方案有个很大的闭端就是需要通过事件监听机制触发JS事件,然后JS通过document获取需要重新渲染的DOM,然后在js的DOM模型上修改数据触发document渲染页面。
在浏览器中document只是提供给JS操作文档模型的接口,双方通讯通道资源有限,基于事件机制触发页面渲染会消耗这个这个通道的大量资源,降低浏览器性能,下面来看看基于数据劫持实现数渲染的模型图(JS与document通讯仅仅只需要一次,而且基于虚拟DOM的支持还可以实现最精准的DOM渲染):
案例需求:分别使用Object数据结构和Array数据结构实现div文本基于input输入实现时时渲染。
二、Object数据劫持实现原理
实现Object数据劫持实现页面渲染的核心原理:
- defineProperty(obj,key,{setter,getter}),其原理可以了解这篇博客:初识JavaScript对象
- 需要注意的是在对数据实现数据监听时,需要对Object实现深度绑定(递归)
1 <input type="text" name="" id="demo"> 2 <div id="show"></div> 3 <script> 4 var oDiv = document.getElementById('show'); 5 var oInput = document.getElementById('demo'); 6 var oData = { 7 valueObj:{ 8 value:'duyi' 9 }, 10 name:'haha' 11 } 12 //输入框事件:触发数据修改(写入) 13 oInput.oninput = function(){ 14 oData.name = this.value; 15 // oData.valueObj.value = this.value; 16 } 17 //修改DOM数据(页面渲染) 18 function upDate(){ 19 oDiv.innerText = oData.name; 20 // oDiv.innerText = oData.valueObj.value; 21 } 22 upDate();//初始数据渲染 23 //给数据绑定监听 24 function Observer(data){ 25 if(!data || typeof data != 'object'){ 26 return data; 27 }; 28 // Object.keys(data)不能获取数组的索引,所以Observer无法实现数据数据监听 29 Object.keys(data).forEach(function(item){ 30 definedRective(data,item,data[item]); // 31 }) 32 } 33 //数据监听:当setter被触发时,修改数据并渲染到页面 34 function definedRective(data,key,val){ 35 Observer(val); //使用递归深度监听对象数据变化,例如:示例数据oData.valueObj.value的监听 36 Object.defineProperty(data,key,{ 37 get(){ 38 return val; 39 }, 40 set(newValue){ 41 if(newValue == val) return; 42 val = newValue; 43 upDate(); //数据渲染到DOM 44 } 45 }) 46 } 47 Observer(oData);//给数据绑定监听方法 48 </script>
三、Array数据劫持的实现原理
基于数组的数据修改方法push、pop、unshift、shift、slice、sort、reverse实现数据劫持:
为什么Array的元素修改不能使用Object.defineProperty(arr,key,{...})实现数据劫持呢?这是因为Array的索引不能使用setter和getter数据描述符,所以无法实现,这也就是在Vue中无法使用Array[index]的方式触发数据渲染的原因。
这里的示例我没有实现全部的数组方法的数据劫持,只是用了push一个方法来实现示例需求:
1 <input type="text" name="" id="demo"> 2 <div id="show"></div> 3 <script> 4 var oDiv = document.getElementById('show'); 5 var oInput = document.getElementById('demo'); 6 let arr = ["duyi"]; 7 let {push} = Array.prototype; 8 console.log(push); 9 function upArrData(){ 10 oDiv.innerText = arr[arr.length-1]; 11 } 12 upArrData(); 13 oInput.oninput = function(){ 14 arr.push(this.value); 15 } 16 Object.defineProperty(Array.prototype,'push',{ 17 value:(function(){ 18 return function(...arg){ 19 push.apply(arr,arg); 20 upArrData(); 21 } 22 })() 23 }); 24 </script>
四、Proxy、Reflect
关于Proxy、Reflect是什么?能做什么?以及应用场景有哪些?先都放到一边,我们先来看看使用Proxy如何实现前面得案例需求:
1 <input type="text" id="demo" name=""> 2 <div id="show"></div> 3 <script> 4 const oInput = document.getElementById("demo"); 5 const oDiv = document.getElementById("show"); 6 7 let oData = { 8 name:"duyi", 9 valueObj:{ 10 value:"aaa" 11 } 12 } 13 function upData(){ 14 oDiv.innerText = oData.name; 15 } 16 upData(); 17 oInput.oninput = function(){ 18 proData.name = this.value; 19 } 20 let proData = new Proxy(oData,{ 21 set(target,key,value,receiver){ 22 Reflect.set(target,key,value); 23 upData(); 24 }, 25 get(target,key,receiver){ 26 return Reflect.get(target,key); 27 } 28 }); 29 </script>
通过下面这个示图来了解Proxy、Reflect实现案例需求的流程图:
当oninput事件触发时,proxy代理data接收数据,然后通过reflect映射给data;然后,当有获取Data数据时,Proxy会将get操作拦截下来,再通过reflect映射出data的真实数据。
Proxy在代码量上远远优于Object.defineProperty()的数据劫持操作,并且可以直接作用数组。
Proxy不能直接对引用值属性做深入代理,需要一个节点一个节点的进行创建proxy对象来实现,所以上面的代码如果替换成这样两条与就不能触发数据渲染:
1 oDiv.innerText = oDiv.valueObj.value; //渲染数据 2 proData.valueObj.value = this.value; //修改数据
Proxy是什么?
Proxy用于修改某些操作的默认行为,同等于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy能做什么?
Proxy可以理解在目标对象前架设一个拦截层,外界对该对象的访问都必须先通过这层拦截,因此可以提供一种机制对外界的访问进行过滤和改写。Proxy词意为“代理”,所以通常也被称为代理器。
Proxy的应用场景有?
数据验证、值修正及附加属性、扩展构造函数等,详细可以了解MDN手册:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
Reflect是什么?
Reflect词意为“反射”,其对象方法与Proxy对象的方法对应,并且也与Object的方法对应,也就是javaScript用来实现映射的API,注意Reflect不能执行new指令。
详细可以了解阮一峰的ES6手册:http://es6.ruanyifeng.com/?search=reflect&x=0&y=0#docs/reflect
1 set(target,key,value,receiver){ 2 //value 写入的值 3 Reflect.set(target,key,value); 4 upData(); 5 }, 6 get(target,key,receiver){ 7 // target--属性所属的对象 8 // key--属性的名称 9 // receiver--代理对象 10 // console.log(target,key,receiver); 11 return Reflect.get(target,key); 12 }
注意Proxy无操作会直接转发代理,可以理解为它内部直接执行了Reflect映射:
let target = {}; let p = new Proxy(target,{}); p.a = 37; console.log(target.a); //37
关于Proxy和Reflect以后再来补充,毕竟这两个API因为不是语法糖,而是浏览器新拓展的全新功能,转码工具无法进行转码,而浏览器的兼容性目前还很糟糕,还不能得到广泛的应用。