Object.defineProperty與proxy進行對比
Object.defineProperty() 和 ES2015 中新增的 Proxy 物件,會經常用來做資料劫持.
資料劫持:在訪問或者修改物件的某個屬性時,通過一段程式碼攔截這個行為,進行額外的操作或者修改返回結果.資料劫持最典型的應用------雙向的資料繫結
- Vue 2.x 利用 Object.defineProperty(),並且把內部解耦為 Observer, Dep, 並使用 Watcher 相連
- Vue 在 3.x 版本之後改用 Proxy 進行實現
Object.defineProperty
先來用Object.defineProperty實現一下物件的攔截。
<html>
<head>
</head>
<body>
<script>
let data = {
m:234,
n:[1,34,4,5676],
h:{
c:34
}
}
function observer(data){
if(typeof data === 'object'){
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])
})
}
}
function defineReactive (obj,key,val){
Object.defineProperty(obj,key,{
get(){
console.log('get',key)
return val
},
set(newVal){
console.log('set',newVal)
if(newVal !== val ) val = newVal
}
})
}
observer(data) // 給data新增監聽
data.m = 111 // 觸發defineReactive()裡的set()
console.log('data.m的值:' , data.m) // 觸發defineReactive()裡的get()
data.h.c = 111 // 這樣不會觸發defineReactive()裡的set(),但觸發了data.h的get()
console.log('data.h.c的值:', data.h.c) // 觸發了data.h的get()
</script>
</body>
</html>
上面通過遍歷data的資料,進行了一次簡單的攔截;看似沒有問題,但如果我們改變data.h.c是不會觸發set鉤子的,為什麼?因為這裡還沒有實現遞迴,所以只攔截了最表面的一層,裡面的則沒有被攔截。
遞迴攔截物件
function defineReactive(obj,key,val){
observer(val)
Object.defineProperty(obj,key,{
get(){
console.log('get')
return val
},
set(newVal){
console.log('set')
if(newVal !== val ) val = newVal
}
})
}
遞迴攔截,只要在defineReactive函式再調一次observer函式把要攔截的值傳給它就行。這樣,就實現了物件的多層攔截。但是呢,現在是攔截不到陣列的,當我們呼叫push,pop等方法它是不會觸發set鉤子的,為什麼?因為Object.defineProperty壓根就不支援陣列的攔截。既然它不支援,那麼我們只能攔截它的這些(‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’)改變自身資料的方法了。
Object.defineProperty陣列的攔截
function arrayMethods(){
const arrProto = Array.prototype
const arrayMethods = Object.create(arrProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach(function (method) {
const original = arrProto[method]
Object.defineProperty(arrayMethods, method, {
value: function v(...args) {
console.log('set arrayMethods')
return original.apply(this, args)
}
})
})
return arrayMethods
}
以上就是對這些陣列的原型方法進行了一個攔截,然後把它覆蓋要攔截的陣列的原型就行,下面簡單修改一下observer
function observer(data){
if(typeof data === 'object'){
if(Array.isArray(data)){
data.__proto__ = arrayMethods()
}else{
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])
})
}
}
}
在vue中,還會判斷該key有沒有__proto__,如果沒有就直接把這些方法放到這個key的自身上,如果有就直接覆蓋這個__proto__。
function arrayMethods(){
const arrProto = Array.prototype
const arrayMethods = Object.create(arrProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach(function (method) {
const original = arrProto[method]
Object.defineProperty(arrayMethods, method, {
value: function v(...args) {
console.log('set arrayMethods')
return original.apply(this, args)
}
})
})
return arrayMethods
}
function observer(data){
if(typeof data === 'object'){
if(Array.isArray(data)){
data.__proto__ = arrayMethods()
}else{
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key])
})
}
}
}
function defineReactive(obj,key,val){
observer(val)
Object.defineProperty(obj,key,{
get(){
console.log('get')
return val
},
set(newVal){
console.log('set')
if(newVal !== val ) val = newVal
}
})
}
observer(data)
以上,就完成了對物件和陣列的攔截(說明:vue2.x中的實現會比這裡複雜,但大概思路和大概實現是這樣),看似辛苦點,換來一個完美的結果挺不錯的。但真的是你想的那樣嗎?試一下呼叫data.n[1] = xxx,它是不會觸發set鉤子的,這也是在proxy出現之前,無能無力的,所以在vue中提供了 s e t , set, set,deleteAPI。
proxy
這裡就不介紹proxy了,就當你對它有了解過了。直接上程式碼
let data = {
m:234,
n:[1,34,4,5676],
h:{
c:34
}
}
function defineReactive(obj){
Object.keys(obj).forEach((key) => {
if(typeof obj[key] === 'object'){
obj[key] = defineReactive(obj[key])
}
})
return new Proxy(obj,{
get(target,key){
console.log('get')
return target[key]
},
set(target,key,val){
console.log('set')
return target[key] = val
}
})
}
data = defineReactive(data)
就這麼一點程式碼就實現了對物件和陣列的攔截(說明:這裡不是vue3的實現方法,vue3怎麼實現的,我還不知道,還沒看過它的原始碼,有興趣自己去看一下,然後順便告訴我一下怎麼實現的,哈哈哈),Object.defineProperty實現不了的,它能實現;Object.defineProperty實現的了,它也能實現。無論你調push,pop等方法它能攔截,你調data.n[1] = xxx也能攔截,簡直不要太爽
最後
這裡只是通過對物件和陣列的攔截,來體驗了一下proxy的威力;proxy能做的遠遠不止這樣。