起源:在 Vue 的資料繫結中會對一個物件屬性的變化進行監聽,並且通過依賴收集做出相應的檢視更新等等。
問題:一個物件所有型別的屬性變化都能被監聽到嗎?
之前用 Object.defineProperty
通過物件的 getter/setter
簡單的實現了物件屬性變化的監聽,並且去通過依賴關係去做相應的依賴處理。
但是,這是存在問題的,尤其是當物件中某個屬性的值是陣列的時候。正如 Vue 文件所說:
由於 JavaScript 的限制,Vue 無法檢測到以下陣列變動:
- 當你使用索引直接設定一項時,例如
vm.items[indexOfItem] = newValue
- 當你修改陣列長度時,例如
vm.items.length = newLength
從 Vue 原始碼中也可以看到確實是對陣列做了特殊處理的。原因就是 ES5 及以下的版本無法做到對陣列的完美繼承 。
實驗一下?
用之前寫好的 observe
做了一個簡單的實驗,如下:
import { observe } from './mvvm'
const data = {
name: 'Jiang',
userInfo: {
gender: 0
},
list: []
}
// 此處直接使用了前面寫好的 getter/setter
observe(data)
data.name = 'Solo'
data.userInfo.gender = 1
data.list.push(1)
console.log(data)
複製程式碼
結果是這樣的:
從結果可以看出問題所在,data
中 name、userInfo、list 屬性的值均發生了變化,但是陣列 list 的變化並沒有被 observe
監聽到。原因是什麼呢?簡單來說,運算元組的方法,也就是 Array.prototype
上掛載的方法並不能觸發該屬性的 setter,因為這個屬性並沒有做賦值操作。
如何解決這個問題?
Vue 中解決這個問題的方法,是將陣列的常用方法進行重寫,通過包裝之後的陣列方法就能夠去在呼叫的時候被監聽到。
在這裡,我想的一種方法與它類似,大概就是通過原型鏈去攔截對陣列的操作,從而實現對運算元組這個行為的監聽。
實現如下:
// 讓 arrExtend 先繼承 Array 本身的所有屬性
const arrExtend = Object.create(Array.prototype)
const arrMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* arrExtend 作為一個攔截物件, 對其中的方法進行重寫
*/
arrMethods.forEach(method => {
const oldMethod = Array.prototype[method]
const newMethod = function(...args) {
oldMethod.apply(this, args)
console.log(`${method}方法被執行了`)
}
arrExtend[method] = newMethod
})
export default {
arrExtend
}
複製程式碼
需要在 defineReactive
函式中新增的程式碼為:
if (Array.isArray(value)) {
value.__proto__ = arrExtend
}
複製程式碼
測試一下:data.list.push(1)
我們看看結果:
上面程式碼的邏輯一目瞭然,也是 Vue 中實現思路的簡化。將 arrExtend
這個物件作為攔截器。首先讓這個物件繼承 Array
本身的所有屬性,這樣就不會影響到陣列本身其他屬性的使用,後面對相應的函式進行改寫,也就是在原方法呼叫後去通知其它相關依賴這個屬性發生了變化,這點和 Object.defineProperty
中 setter
所做的事情幾乎完全一樣,唯一的區別是可以細化到使用者到底做的是哪一種操作,以及陣列的長度是否變化等等。
還有什麼別的辦法嗎?
ES6 中我們看到了一個讓人耳目一新的屬性——Proxy
。我們先看一下概念:
通過呼叫 new Proxy() ,你可以建立一個代理用來替代另一個物件(被稱為目標),這個代理對目標物件進行了虛擬,因此該代理與該目標物件表面上可以被當作同一個物件來對待。
代理允許你攔截在目標物件上的底層操作,而這原本是 JS 引擎的內部能力。攔截行為使用了一個能夠響應特定操作的函式(被稱為陷阱)。
Proxy
顧名思義,就是代理的意思,這是一個能讓我們隨意玩弄物件的特性。當我們,通過Proxy
去對一個物件進行代理之後,我們將得到一個和被代理物件幾乎完全一樣的物件,並且可以對這個物件進行完全的監控。
什麼叫完全監控?Proxy
所帶來的,是對底層操作的攔截。前面我們在實現對物件監聽時使用了Object.defineProperty
,這個其實是 JS 提供給我們的高階操作,也就是通過底層封裝之後暴露出來的方法。Proxy
的強大之處在於,我們可以直接攔截對代理物件的底層操作。這樣我們相當於從一個物件的底層操作開始實現對它的監聽。
改進一下我們的程式碼?
const createProxy = data => {
if (typeof data === 'object' && data.toString() === '[object Object]') {
for (let k in data) {
if (typeof data[k] === 'object') {
defineObjectReactive(data, k, data[k])
} else {
defineBasicReactive(data, k, data[k])
}
}
}
}
function defineObjectReactive(obj, key, value) {
// 遞迴
createProxy(value)
obj[key] = new Proxy(value, {
set(target, property, val, receiver) {
if (property !== 'length') {
console.log('Set %s to %o', property, val)
}
return Reflect.set(target, property, val, receiver)
}
})
}
function defineBasicReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
return value
},
set(newValue) {
if (value === newValue) return
console.log(`發現 ${key} 屬性 ${value} -> ${newValue}`)
value = newValue
}
})
}
export default {
createProxy
}
複製程式碼
對於一個物件中的基礎型別的屬性,我們還是通過Object.defineProperty
來實現響應式的屬性,因為這裡並不存在痛點,但是在實現對Object
型別的屬性進行監聽的時候,我採用的是建立代理,因為我們之前的痛點在於無法去有效監聽陣列的變化。當我們使用這種改進方法之後,我們不用像之前通過重寫陣列的方法來實現對陣列操作的監聽了,因為之前這種方法存在很多的侷限性,我們不能覆蓋所有的陣列操作,同時,我們也不能響應到類似於data.array.length = 0
這種操作。通過代理實現之後,一切都不一樣了。我們可以從底層就實現對陣列的變化進行監聽。甚至能watch
到陣列長度的變化等等各種更加細節的東西。這無疑解決了很大的問題。
我們呼叫一下剛才的方法,試試看?
let data = {
name: 'Jiang',
userInfo: {
gender: 0,
movies: []
},
list: []
}
createProxy(data)
data.name = 'Solo'
data.userInfo.gender = 0
data.userInfo.movies.push('星際穿越')
data.list.push(1)
複製程式碼
輸出為:
結果非常完美~我們實現了對物件所有屬性變化的監聽Proxy
的騷操作還有很多很多,比如說將代理當作原型放到原型鏈上,這樣一來就可以只對子類不含有的屬性進行監聽,非常的強大。Proxy
可以得到更加廣泛的應用,而且場景很多。這也是我第一次去使用,還需要多加鞏固( ;´Д`)