- 什么是數據劫持
- 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因為不是語法糖,而是瀏覽器新拓展的全新功能,轉碼工具無法進行轉碼,而瀏覽器的兼容性目前還很糟糕,還不能得到廣泛的應用。