Skip to content

数据劫持

使用过 Vue 框架的应该就不陌生了,因为 Vue 中的响应式原理就是 数据劫持 和 订阅发布模式。而这里主要讲的是 数据劫持

数据劫持:在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。最典型的使用场景就是响应式。

  1. 在 Vue 2.x 利用 Object.defineProperty()。(具体想知道 vue 中怎么实现的可以百度一下,网上都一大把)
  2. 在 Vue 3.x 版本之后改用 Proxy 进行实现。

基础

首先说一下两个语法:

Object.defineProperty(obj, prop, descriptor)

该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • obj 要定义属性的对象

  • prop 要定义或修改的属性的名称或 Symbol

  • descriptor 要定义或修改的属性描述符

  • 返回值:被传递给函数的对象

详情 → 传送门

小DEMO

javascript
const data = {}

let a = 2;

Object.defineProperty(data, 'a', {
  enumerable: true,
  configurable: false,
  get: () => {
    console.log('get:', a)
    return a
  },
  set: (newValue) => {
    console.log('set:', newValue)
    a = newValue
  }
})

data.a = 10
console.log(data.a)

// 输出:
// set: 10
// get: { a: 10 }
// 10

new Proxy(target, handler)

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

  • target 要使用 Proxy 包装的目标对象(可以使任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

详情 → 传送门

小DEMO

javascript
const handle = {
  get(obj, prop, value) {
    console.log('get:', value)
    return value
  },
  set(obj, prop, value) {
    obj[prop] = value
    console.log('set:', value)
    return true
  }
}

const data = {
  a: 1
}

const proxy = new Proxy(data, handle);
proxy.a = 10
console.log(proxy.a)

// 输出:
// set: 10
// get: { a: 10 }
// 10

Object.defineProperty 对比 Proxy

Object.defineProperty的问题有三个:

  • 不能监听数组的变化(如:push, pop, shift, unshift,splice, sort, reverse 并不会触发 set)
  • 不能监听对象,即必须遍历对象的每个属性。(通常配合 Object.keys 来遍历)
  • 必须深层遍历嵌套的对象

Proxy优点:

  • 可以监听整个对象,而不是对象的某个属性,但深层对象还是无法监听。
  • 支持数组。
  • 支持更多的捕捉器
  • 会受浏览器厂商更多的关注和性能优化

缺点:

  • ES6新语法
  • 无法使用 polyfill 来兼容(即兼容性需要考虑)

利用 Proxy 实现类似 Vue 中的数据挟持

javascript
class Observe {
  constructor(data) {
    this.handle = {
      set: (target, property, value) => {
        console.log('set:', value)
        // 判断新值是否等于旧值
        if (target[property] === value) {
          return false;
        }
        target[property] = value;
        // TODO 数据变化,通知所有订阅者
        return true;
      },
      get: (target, property) => {
        console.log('get:', target[property])
        return target[property];
      }
    }
    // 监听每一个数据
    this.data = this.defineReactive(data);
  }

  // 利用proxy监听data,循环遍历监听data下所有对象,并返回proxy
  // 注:因为proxy与Object.defineProperty不同,可以监听一个对象,但一个对象的深层对象会监听不到,所有需要遍历监听所有引用类型的。
  defineReactive(data) {
    if (!data || typeof data !== "object") {
      return data;
    }

    Object.keys(data).forEach(key => {
      data[key] = this.defineReactive(data[key]);
    });

    return new Proxy(data, this.handle);
  }
}

const data = {
  a: 1,
  b: {
    c: 2,
    d: {
      e: 3
    }
  }
}

const proxy = new Observe(data).data

console.log(proxy.b.d.e)
proxy.b.d.e = 10
console.log(proxy.b.d.e)

// 输出:
// 第一步操作 console.log(proxy.b.d.e)
// get: { c: 2, d: { e: 3 } }
// get: { e: 3 }
// get: 3
// 3
// 第二步操作 proxy.b.d.e = 10
// get: { c: 2, d: { e: 3 } }
// get: { e: 3 }
// set: 10
// 第三步操作 console.log(proxy.b.d.e)
// get: { c: 2, d: { e: 10 } }
// get: { e: 10 }
// get: 10
// 10

Released under the MIT License.