前瞻
當前前端界空前繁榮,各種框架橫空出世,包括各類mvvm框架橫行霸道,比如Anglar,Regular,Vue,React等等,它們最大的優點就是可以實現數據綁定,再也不需要手動進行DOM操作了,它們實現的原理也基本上是臟檢查或數據劫持。那么本文就以Vue框架出發,探索其中數據劫持的奧秘(本文所選取的相關代碼源自於Vue v2.0.3版本的源碼)。
什么是數據劫持
首先我們應該搞清楚什么是數據劫持,說白了就是通過Object.defineProperty()來劫持對象屬性的setter和getter操作,在數據變動時做你想要做的事情,舉個栗子:
var data = {
name:'lhl'
}
Object.keys(data).forEach(function(key){
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
console.log('get');
},
set:function(){
console.log('監聽到數據發生了變化');
}
})
});
data.name //控制台會打印出 “get”
data.name = 'hxx' //控制台會打印出 "監聽到數據發生了變化"
上面的這個栗子可以看出,我們完全可以控制對象屬性的設置和讀取。在Vue中,作者在很多地方都非常巧妙的運用了defineProperty這個方法,具體用在哪里並且它又解決了哪些問題,下面做詳細的介紹:
監聽對象屬性的變化
這個應該是Vue非常重要的一塊,其主要思想是observer每個對象的屬性,添加到訂閱器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)
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
//這里也是作者一個巧妙設計,在創建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()//這個是真正劫持的目的,要對訂閱者發通知了
}
})
}
以上是監聽對象屬性的變化,那么下面再看看如何監聽數組的變化:
監聽數組的變化
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
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__
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
})
})
...
/**
* Define a property.
*/
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
通過上面的代碼可以看出Vue是通過修改了數組的幾個操作的原型來實現的。
實現對象屬性代理
正常情況下我們是這樣實例化一個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()這個方法來實現了數據的監聽和修改,同時也達到了很好的模塊間解耦,在日常開發用好這個方法說不定會達到令人意想不到的結果。
本篇文章是我對Vue的淺薄之悟,如有理解不足之處,還請大家批評指正,Thank you ~