起源:在 Vue 的數據綁定中會對一個對象屬性的變化進行監聽,而且經過依賴收集作出相應的視圖更新等等。javascript
問題:一個對象全部類型的屬性變化都能被監聽到嗎?vue
以前用 Object.defineProperty
經過對象的 getter/setter
簡單的實現了對象屬性變化的監聽,而且去經過依賴關係去作相應的依賴處理。java
可是,這是存在問題的,尤爲是當對象中某個屬性的值是數組的時候。正如 Vue 文檔所說:git
因爲 JavaScript 的限制,Vue 沒法檢測到如下數組變更:github
- 當你使用索引直接設置一項時,例如
vm.items[indexOfItem] = newValue
- 當你修改數組長度時,例如
vm.items.length = newLength
從 Vue 源碼中也能夠看到確實是對數組作了特殊處理的。緣由就是 ES5 及如下的版本沒法作到對數組的完美繼承 。數組
用以前寫好的 observe
作了一個簡單的實驗,以下:app
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)
複製代碼
結果是這樣的:mvvm
從結果能夠看出問題所在,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
能夠獲得更加普遍的應用,並且場景不少。這也是我第一次去使用,還須要多加鞏固( ;´Д`)