顧名思義,數據響應式就是當咱們修改數據時,能夠監聽到這個修改,而且做出相應的響應。javascript
需求:當咱們修改 obj
對象時,觸發 update
方法。java
思路:使用 Object.defineProperty
對數據進行劫持,每次修改的時候都會執行 set
方法,在 set
內部能夠進行響應更新api
編寫初版代碼:數組
function isObject(obj){
return obj.constructor === Object
}
function update(){ // 更新響應
console.log('updated!')
}
function observer(obj){ // 監測對象
if(!isObject(obj)) return
for(let key in obj){ // 對每一個屬性進行 Object.defineProperty 定義
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, value){ // 數據劫持
Object.defineProperty(obj, key, {
get(){
return value
},
set(newValue){ // 修改時,觸發 update 方法
update()
value = newValue
}
})
}
let obj = {a: 1}
observer(obj)
obj.a = 3 // updated!
複製代碼
當咱們修改 obj 中經過 Object.defineProperty
定義的屬性時,會觸發 set
方法,觸發更新。app
初版編寫完成,已經實現了基礎功能,可是有兩個問題:ui
對於形如 {a: {b: 1}}
嵌套的對象,沒法進行任意深度的監測,由於沒法知道對象嵌套了幾層,只能用遞歸進行監測。this
修改的後值若是是一個對象,須要對這個對象也進行監測spa
obj.a = {c: 1}
obj.a.c = 3 // expected: updated!
複製代碼
咱們對 defineReactive
進行一點修改便可:prototype
function defineReactive(obj, key, value){
observer(value) // 利用遞歸深度劫持:若是 value 仍是對象,繼續定義,直到 isObject 返回 false
Object.defineProperty(obj, key, {
get(){
return value
},
set(newValue){
if(isObject(newValue)){ // 若是新值爲對象,對新值進行進行數據監測
observer(newValue)
}
update()
value = newValue
}
})
}
複製代碼
至此,咱們實現了對對象數據的監測,當修改對象上的屬性時,能夠觸發響應,而且這個對象能夠是任意嵌套深度的,修改的新值也能夠是任意深度嵌套的對象。代理
不足之處:給對象新增一個不存在的屬性時,沒法觸發響應。
需求:當咱們使用 push pop shift unshift reverse sort splice
方法修改數組時,會觸發更新。
數組不能像對象那樣用 Object.defineProperty
劫持修改,因此咱們只能在上面說的這些方法上面下手,咱們能夠對這些方法進行重寫。
可是要注意的是:重寫不能夠對使用這些 api 的其餘地方產生影響
這裏咱們建立一個新的 Array 原型,而後改變須要監測的數組的原型,指向新的原型 ResponsiveArray
const ResponsiveArray = Object.create(Array.prototype); // 建立新的 Array 原型
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
// 對每一個方法進行重寫,掛載到 ResponsiveArray 上
ResponsiveArray[method] = function() {
update()
Array.prototype[method].apply(this, arguments)
}
})
function observer(obj){
if(Array.isArray(obj)){
return Object.setPrototypeOf(obj, ResponsiveArray) // 改變原型
}
}
function update(){
console.log('updated!')
}
let arr = [1,2,3,4]
observer(arr)
arr.push(1,2,3) // updated!
複製代碼
以上,就實現了對普通對象和數組的監測。完整代碼以下:
// 建立新的 Array 原型
const ResponsiveArray = Object.create(Array.prototype);
// 在新原型上重寫數組方法
['pop', 'push', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
ResponsiveArray[method] = function() {
update()
Array.prototype[method].apply(this, arguments)
}
})
function update(){
console.log('updated!')
}
function isObject(obj){
return obj.constructor === Object
}
function observer(obj){
if(Array.isArray(obj)){
return Object.setPrototypeOf(obj, ResponsiveArray) // 改變數組的原型
}
if(!isObject(obj)) return
for(let key in obj){ // 對普通對象的每一個屬性進行監測
defineReactive(obj, key, obj[key])
}
}
function defineReactive(obj, key, value){// 數據劫持
observer(value) // 遞歸調用,使得任意深度的對象能夠被監測到
Object.defineProperty(obj, key, {
get(){
return value
},
set(newValue){
if(isObject(newValue)){ // 對修改後爲對象的新值進行監測
observer(newValue)
}
update()
value = newValue
}
})
}
複製代碼
function update(){
console.log('updated')
}
let obj = [1,2,3]
const proxyObj = new Proxy(obj, {
set(target, key, value){
if(key === 'length') return true // ①
update()
return Reflect.set(target, key, value)
},
get(target, key){
return Reflect.get(target, key)
}
})
proxyObj.push(12)
proxyObj[1] = 'xxx'
複製代碼
與 defineProperty 的區別:
須要注意的點是:修改數組元素時,除了插入元素以外,還會修改 length
屬性,觸發兩次更新,若是想避免修改 length
觸發更新,能夠加上上面的①,對 length
的修改進行過濾。
但不足的是:此時不能實現任意嵌套深度的對象的代理。
由於對於形如 proxyObj.a.b = 1
的語句,首先會返回 proxyObj.a
,對返回值上的 b
進行修改,沒有通過代理,因此也不會觸發更新。
因此咱們只須要在返回的時候,返回通過 proxy 代理的值便可。
const handler = {
set(target, key, value){
if(key === 'length') return true
update()
return Reflect.set(target, key, value)
},
get(target, key){
if(typeof target[key] === 'object'){
return new Proxy(target[key], handler) // 只要獲取的是對象,就返回通過代理後的對象。
}
return Reflect.get(target, key)
}
}
let proxyObj = new Proxy(obj, handler)
proxyObj.b.c = 'xxx'
複製代碼