如何做数据拦截
在 JavaScript 中,数据拦截是一种技术,可以拦截对象属性的读取、赋值等操作,从而可以对这些操作进行控制和定制。比如在Vue中 就是使用数据拦截,作为响应式的实现方式,来帮助Vue 以尽量小的颗粒度去render,减少性能损耗。 Vue2Github 相关资料
所以这篇文章也是简单 总结下,自己忽然想到的 这部分知识。
Vue2 的数据拦截
总所周知 Vue2 中是使用
Object.definePrototype来 拦截属性的 getter 和setter 操作。 简单代码如下
function defineReactive(obj, key, val, cb) {
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取属性 getter 获取 setter. 如果本身属性 就是拦截属性
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter(v) {
// 最新值
const value = getter ? getter.call(obj) : val
// 未改变直接 return
if (value === v) return
if (setter) {
setter.call(obj, v)
} else {
val = v
}
cb && cb.call(obj, v)
}
})
}
上面拦截属性的意思是,如果对象是如下. p 就是拦截属性,按理说 set 也没有啥用处 -。-
const o = {
get p() {
return 123123
}
}
上面就是 Vue2 中数据拦截 bject.definePrototype 的使用方式,针对特定属性, 来进行 拦截setter 方法, 但是这边只是 针对特定对象的 某一个属性进行 拦截。 所以,Vue2 中会 特定去遍历所有属性进行 处理。
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
function observe(
value,
cb
) {
// 基本属性 或者是 null 就直接 return value吧
if (typeof value !== 'object' || value === null) return value
// 如果已经 是响应式数据了就 直接返回 响应式对象好了
if (value && hasOwn(value, '__ob__')) {
return value
}
defineReactive(value, '__ob__', true)
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// 接着去 拦截 value[key]
defineReactive(value, key, observe(value[key], cb), cb)
}
return value
}
上面
observe就完成对 数据的拦截, 针对 对象,就会 遍历所有的key,针对key 对象的 value 也会进行拦截。
但是上述也有 很多弊端,
- 只能对属性进行劫持:Object.defineProperty 只能对对象的属性进行劫持,无法对整个对象进行劫持,也无法劫持新增的属性。
-
深度监听性能问题:当需要对深层嵌套的对象进行监听时,由于需要递归遍历整个对象,会导致性能问题。
-
无法监听数组变化:Object.defineProperty 无法监听数组的变化,Vue2 底层是重载了对应的 数组方法。
上面也可以简单优化下,对于set 里面的 value 可以在进行 observe. 最终代码如下
function defineReactive(obj, key, val, cb) {
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 获取属性 getter 获取 setter. 如果本身属性已经被 拦截,就直接拦截function 好了
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter(v) {
// 最新值 这里就对 v 进行observe
const obv = observe(v, cb)
const value = getter ? getter.call(obj) : val
// 未改变直接 return
if (value === obv) return
if (setter) {
setter.call(obj, obv)
} else {
val = obv
}
cb && cb.call(obj, obv)
}
})
}
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
function observe(
value,
cb
) {
// 基本属性 或者是 null 就直接 return value吧
if (typeof value !== 'object' || value === null) return value
// 如果已经 是响应式数据了就 直接返回 响应式对象好了
if (value && hasOwn(value, '__ob__')) {
return value
}
defineReactive(value, '__ob__', true)
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// 接着去 拦截 value[key]
defineReactive(value, key, observe(value[key], cb), cb)
}
return value
}
Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
具体用法和详情可以查看这篇文章 Proxy
// cachedData 是为了防止 已经 proxy 代理过的数据 重复 代理
const cachedData = new WeakMap();
function handleProxyData<T extends Record<any, any>>(data: T, cb: (d: T) => any): T {
if (typeof data !== "object" || data === null) return data;
const t = cachedData.get(data);
if (t) return t;
const proxyObj = new Proxy(data, {
get(target, key) {
return handleProxyData(Reflect.get(target, key), cb);
},
set(target, key, val) {
const originVal = Reflect.get(target, key);
const isSetSuc = Reflect.set(target, key, handleProxyData(val, cb));
originVal !== val && cb && cb(target);
return isSetSuc;
},
});
cachedData.set(proxyObj, proxyObj);
cachedData.set(data, proxyObj);
return proxyObj;
}
上面就是 一次简单的 Proxy 代理,实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。 所以其实这个对于数组也有很好的 支持。
上面 就可以动态的拦截掉新增的属性
上面 就可以拦截 数组的更新,其实是 push 操作会更改数组的 length,就会触发 proxy的 拦截。