每日一个前端小方法

145 阅读4分钟

如何做数据拦截

在 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 也会进行拦截。

image.png

但是上述也有 很多弊端,

  1. 只能对属性进行劫持:Object.defineProperty 只能对对象的属性进行劫持,无法对整个对象进行劫持,也无法劫持新增的属性。

image.png

  1. 深度监听性能问题:当需要对深层嵌套的对象进行监听时,由于需要递归遍历整个对象,会导致性能问题。

  2. 无法监听数组变化: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)了点运算符,即用自己的定义覆盖了语言的原始定义。 所以其实这个对于数组也有很好的 支持。

image.png 上面 就可以动态的拦截掉新增的属性

image.png 上面 就可以拦截 数组的更新,其实是 push 操作会更改数组的 length,就会触发 proxy的 拦截。

参考文献