顾名思义,数据响应式就是当咱们修改数据时,能够监听到这个修改,而且做出相应的响应。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'
复制代码