前端界空前繁榮,各種框架橫空出世,包括各類mvvm框架橫行霸道,比如Angular、Regular、Vue、React等等,它們最大的優點就是可以實現數據綁定,再也不需要手動進行DOM操作了,它們實現的原理也基本上是臟檢查或數據劫持。那么本文就以Vue框架出發,探索作者運用Object.defineProperty來實現數據劫持的奧秘。
回顧一下Object.defineProperty
-
語法
Object.defineProperty(obj,prop,descriptor)
-
參數
obj:目標對象
prop:需要定義的屬性或方法的名稱
descriptor:目標屬性所擁有的特性
-
可供定義的特性列表
value:屬性的值
writable:如果為false,屬性的值就不能被重寫。
get: 一旦目標屬性被訪問就會調回此方法,並將此方法的運算結果返回用戶。
set:一旦目標屬性被賦值,就會調回此方法。
configurable: 如果為false,則任何嘗試刪除目標屬性或修改屬性性以下特性(writable, configurable, enumerable)的行為將被無效化。
enumerable: 是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。
什么是數據劫持
通過上面對Object.defineProperty的介紹,我們不難發現,當我們訪問或設置對象的屬性的時候,都會觸發相對應的函數,然后在這個函數里返回或設置屬性的值。
既然如此,我們當然可以在觸發函數的時候動一些手腳做點我們自己想做的事情,這也就是“劫持”操作。
在Vue中其實就是通過Object.defineProperty來劫持對象屬性的setter和getter操作,並“種下”一個監聽器,當數據發生變化的時候發出通知。
舉個栗子:
var data = { name:'lhl' } Object.keys(data).forEach(function(key){ Object.defineProperty(data,key,{ enumerable:true, // 是否能在for...in循環中遍歷出來或在Object.keys中列舉出來。 configurable:true, // false,不可修改、刪除目標屬性或修改屬性性以下特性 get:function(){ console.log('get'); }, set:function(){ console.log('監聽到數據發生了變化'); } }) }); data.name //控制台會打印出 “get” data.name = 'hxx' //控制台會打印出 "監聽到數據發生了變化"
上面的這個例子可以看出,我們完全可以控制對象屬性的設置和讀取。
在Vue中,作者在很多地方都非常巧妙的運用了Object.defineProperty這個方法,具體用在哪里並且它又解決了哪些問題,下面就做詳細的介紹:
監聽對象屬性的變化
這個應該是Vue敲開數據綁定的前大門,它通過observe(觀察)每個對象的屬性,添加到訂閱器dep中,當數據發生變化的時候發出一個notice(預告)。 相關源代碼如下:(作者采用的是ES6+flow寫的,代碼在src/core/observer/index.js模塊里面)
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: Function ) { const dep = new Dep()//創建訂閱對象 const property = Object.getOwnPropertyDescriptor(obj, key)//獲取obj對象的key屬性的描述 //屬性的描述特性里面如果configurable為false則屬性的任何修改將無效 if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set let childOb = observe(val)//創建一個觀察者對象 Object.defineProperty(obj, key, { enumerable: true,//可枚舉 configurable: true,//可修改 get: function reactiveGetter () { const value = getter ? getter.call(obj) : val//先調用默認的get方法取值 //這里就劫持了get方法,也是作者一個巧妙設計,在創建watcher實例的時候,通過調用對象的get方法往訂閱器dep上添加這個創建的watcher實例 if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() } if (Array.isArray(value)) { dependArray(value) } } return value//返回屬性值 }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val//先取舊值 if (newVal === value) { return } //這個是用來判斷生產環境的,可以無視 if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = observe(newVal)//繼續監聽新的屬性值 dep.notify()//這個是真正劫持的目的,要對訂閱者發通知了 } }) }
以上是Vue監聽對象屬性的變化,那么問題來了,我們經常在傳遞數據的時候往往不是一個對象,很有可能是一個數組,那是不是就沒有辦法了呢,答案顯然是否則的。那么下面就看看作者是如何監聽數組的變化:
監聽數組的變化
我們還看先看這段源碼:
const arrayProto = Array.prototype//原生Array的原型 export const arrayMethods = Object.create(arrayProto) ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { const original = arrayProto[method]//緩存元素數組原型 //這里重寫了數組的幾個原型方法 def(arrayMethods, method, function mutator () { //這里備份一份參數應該是從性能方面的考慮 let i = arguments.length const args = new Array(i) while (i--) { args[i] = arguments[i] } const result = original.apply(this, args)//原始方法求值 const ob = this.__ob__//這里this.__ob__指向的是數據的Observer let inserted switch (method) { case 'push': inserted = args break case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) }) ... //定義屬性 function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); }
上面的代碼主要是繼承了Array本身的原型方法,然后又做了劫持修改,可以發出通知。
Vue在observer數據階段會判斷如果是數組的話,則修改數組的原型,這樣的話,后面對數組的任何操作都可以在劫持的過程中控制。
結合Vue的思想,我簡單的寫個小demo方便更好的理解:
var arrayMethod = Object.create(Array.prototype); ['push','shift'].forEach(function(method){ Object.defineProperty(arrayMethod,method,{ value:function(){ var i = arguments.length var args = new Array(i) while (i--) { args[i] = arguments[i] } var original = Array.prototype[method]; var result = original.apply(this,args); console.log("已經控制了,哈哈"); return result; }, enumerable: true, writable: true, configurable: true }) }) var bar = [1,2]; bar.__proto__ = arrayMethod; bar.push(3);//控制台會打印出 “已經控制了,哈哈”;並且bar里面已經成功的添加了成員 ‘3’
整個過程看起來好像沒有什么問題,似乎Vue已經做到了完美,其實不然,Vue還是不能檢測到數據項和數組長度改變的變化,例如下面的調用:
vm.items[index] = "xxx"; vm.items.length = 100;
我們盡量避免這樣的調用方式,如果確實需要,作者也幫我們實現了一個$set操作,這里就不做介紹了。
實現對象屬性代理
正常情況下我們是這樣實例化一個Vue對象:
var VM = new Vue({ data:{ name:'lhl' }, el:'#id' })
按理說我們操作數據的時候應該是VM.data.name = ‘hxx’才對,但是作者覺得這樣不夠簡潔,所以又通過代理的方式實現了VM.name = ‘hxx’的可能。 相關代碼如下:
function proxy (vm, key) { if (!isReserved(key)) { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get: function proxyGetter () { return vm._data[key] }, set: function proxySetter (val) { vm._data[key] = val; } }); } }
表面上看起來我們是在操作VM.name,實際上還是通過Object.defineProperty()中的get和set方法劫持實現的。
總結
Vue框架很好的利用了Object.defineProperty()這個方法來實現了數據的雙向綁定,同時也達到了很好的模塊間解耦,在日常開發中,你也可以用好這個方法來優化對象獲取和修改屬性方式,或者自己實現一個MVVM的雙向數據綁定等。
僅供學習參考,侵權刪
原文地址:https://juejin.im/entry/589ff26486b599006b3dea9b