JavaScript 之实现响应式数据
更多个人博客:(https://github.com/zenglinan/blog)
如果对你有帮助,欢迎star。
数据响应式:
顾名思义,数据响应式就是当我们修改数据时,可以监听到这个修改,并且作出相应的响应。
一. 监测 Object 对象
需求:当我们修改 obj
对象时,触发 update
方法。
思路:使用 Object.defineProperty
对数据进行劫持,每次修改的时候都会执行 set
方法,在 set
内部可以进行响应更新
编写第一版代码:
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
方法,触发更新。
第一版编写完成,已经实现了基础功能,但是有两个问题:
-
对于形如
{a: {b: 1}}
嵌套的对象,无法进行任意深度的监测,因为无法知道对象嵌套了几层,只能用递归进行监测。 -
修改的后值如果是一个对象,需要对这个对象也进行监测
obj.a = {c: 1}
obj.a.c = 3 // expected: updated!
我们对 defineReactive
进行一点修改即可:
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
}
})
}
三. 利用 proxy 进行代理
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 的区别:
- 可以对添加新属性进行代理
- 无需额外操作即可对数组进行代理,包括 push pop 等方法,以及修改指定索引的元素
需要注意的点是:修改数组元素时,除了插入元素之外,还会修改 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'
感谢你看到了这里,更多个人博文戳这
本文正在参与“写编程博客瓜分千元现金”活动,关注公众号“饥人谷”回复“编程博客”参与活动。