Skip to content

Proxy-Reflect 和响应式原理

Proxy/Reflect

监听对象的基本操作

js
const obj = {
  name: 'hjf',
  age: 18
}

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    set: function (newValue) {
      console.log(`监听到给${key}设置值`)
      value = newValue
    },
    get: function () {
      console.log(`监听到获取${key}的值`)
      return value
    }
  })
})

obj.name = 'kobe'
obj.age = 30

console.log(obj.name)
console.log(obj.age)

输出:

sh
监听到给name设置值
监听到给age设置值
监听到获取name的值
kobe
监听到获取age的值
30

上面这段代码就利用了前面讲过的 Object.defineProperty 的存储属性描述符来对属性的操作进行监听。

但是这样做有什么缺点呢?

  • 首先,Object.defineProperty 设计的初衷,不是为了去监听截止一个对象中所有的属性的。
    • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
  • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty 是无能为力的。

所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

Proxy 基本使用

在 ES6 中,新增了一个 Proxy 类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的:

  • 也就是说,如果我们希望监听一个对象的相关操作,那么我们可以先创建一个代理对象(Proxy 对象);
  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以知道我们想要对原对象进行哪些操作;

我们可以将上面的案例用 Proxy 来实现一次:

  • 首先,我们需要 new Proxy 对象,并且传入需要侦听的对象以及一个处理对象,可以称之为 handler;
    • const p = new Proxy(target, handler)
  • 其次,我们之后的操作都是直接对 Proxy 的操作,而不是原有的对象,因为我们需要在 handler 里面进行侦听;
js
const obj = {
  name: 'hjf',
  age: 18
}

const objProxy = new Proxy(obj, {})

objProxy.name = 'kobe'
objProxy.age = 30

console.log(objProxy.name) // kobe
console.log(objProxy.age) // 30

如果我们想要侦听某些具体的操作,那么就可以在 handler 中添加对应的捕捉器(Trap):

  • set 和 get 分别对应的是函数类型;
  • set 函数有四个参数:
    • target:目标对象(侦听的对象);
    • property:将被设置的属性 key;
    • value:新属性值;
    • receiver:调用的代理对象;
  • get 函数有三个参数:
    • target:目标对象(侦听的对象);
    • property:被获取的属性 key;
    • receiver:调用的代理对象;
js
const obj = {
  name: 'hjf',
  age: 18
}

const objProxy = new Proxy(obj, {
  set: function (target, key, value) {
    console.log(`侦听到代理对象被set操作`, target, key, value)
  },
  get: function (target, key) {
    console.log(`侦听到代理对象被get操作`, target, key)
  }
})

objProxy.name = 'kobe'
objProxy.age = 30

console.log(objProxy.name)
console.log(objProxy.age)
sh
侦听到代理对象被set操作 { name: 'hjf', age: 18 } name kobe
侦听到代理对象被set操作 { name: 'hjf', age: 18 } age 30
侦听到代理对象被get操作 { name: 'hjf', age: 18 } name
undefined
侦听到代理对象被get操作 { name: 'hjf', age: 18 } age
undefined

Proxy 其他捕捉器

Proxy 一共有 13 个捕捉器:

  • 所有的捕捉器都是可选的,如果没有定义某个捕捉器,那么就会保留对象的默认行为; 13 个活捉器分别是做什么的呢?

  • handler.getPrototypeOf()

    • Object.getPrototypeOf 方法的捕捉器。
  • handler.setPrototypeOf()

    • Object.setPrototypeOf 方法的捕捉器。
  • handler.isExtensible()

    • Object.isExtensible 方法的捕捉器。
  • handler.preventExtensions()

    • Object.preventExtensions 方法的捕捉器。
  • handler.getOwnPropertyDescriptor()

    • Object.getOwnPropertyDescriptor 方法的捕捉器。
  • handler.defineProperty()

    • Object.defineProperty 方法的捕捉器。
  • handler.ownKeys()

    • Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  • handler.has()

    • in 操作符的捕捉器。
  • handler.get()

    • 属性读取操作的捕捉器。
  • handler.set()

    • 属性设置操作的捕捉器。
  • handler.deleteProperty()

    • delete 操作符的捕捉器。
  • handler.apply()

    • 函数调用操作的捕捉器。
  • handler.construct()

    • new 操作符的捕捉器。
js
const obj = {
  name: 'hjf',
  age: 18
}

const objProxy = new Proxy(obj, {
  has: function (target, key) {
    console.log('has捕捉器', key)
    return key in target
  },
  set: function (target, key, value) {
    console.log('set捕捉器', key)
    target[key] = value
  },
  get: function (target, key) {
    console.log('get捕捉器', key)
    return target[key]
  },
  deleteProperty: function (target, key) {
    console.log('delete捕捉器')
    delete target[key]
  }
})

console.log('name' in objProxy)
objProxy.name = 'kobe'
console.log(objProxy.name)
delete objProxy.name

输出结果:

sh
has捕捉器 name
true
set捕捉器 name
get捕捉器 name
kobe
delete捕捉器

当然,我们还会看到捕捉器中还有 construct 和 apply,它们是应用于函数对象的:

js
function foo() {
  console.log('foo函数被调用了', this, arguments)
  return 'foo'
}

const fooProxy = new Proxy(foo, {
  apply: function (target, thisArg, otherArgs) {
    console.log('函数的apply侦听')
    return target.apply(thisArg, otherArgs)
  },
  construct(target, argArray, newTarget) {
    console.log(target, argArray, newTarget)
    return new target()
  }
})

const result = fooProxy.apply({ name: 'hjf' }, ['aaa', 'bbb'])
console.log(result)

const f = new fooProxy('abc', 'cba')
console.log(f)

输出结果:

sh
函数的apply侦听
foo函数被调用了 { name: 'hjf' } [Arguments] { '0': 'aaa', '1': 'bbb' }
foo
[Function: foo] [ 'abc', 'cba' ] [Function: foo]
foo函数被调用了 foo {} [Arguments] {}
foo {}

响应式原理

监听对象的变化

  • 方式一:通过 Object.defineProperty 的方式(vue2 采用的方式);
  • 方式二:通过 new Proxy 的方式(vue3 采用的方式);
js
const targetMap = new WeakMap()
function getDepends(obj, key) {
  // 根据对象获取对应的Map对象
  let objMap = targetMap.get(obj)
  if (!objMap) {
    objMap = new Map()
    targetMap.set(obj, objMap)
  }

  // 根据key获取Depend对象
  let depend = objMap.get(key)
  if (!depend) {
    depend = new Depend()
    objMap.set(key, depend)
  }
  return depend
}

我们这里先以 Proxy 的方式来监听:

js
function reactive(obj) {
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      const dep = getDepends(target, key)
      dep.depend()
      return Reflect.get(target, key, receiver)
    },
    set: function (target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      const dep = getDepends(target, key)
      dep.notify()
    }
  })
}
proxyObj.name = 'lilei'

Vue2 响应式原理

我们前面所实现的响应式的代码,其实就是 Vue3 中的响应式原理:

  • Vue3 主要是通过 Proxy 来监听数据的变化以及收集相关的依赖的;
  • Vue2 中通过我们前面学习过的 Object.defineProerty 的方式来实现对象属性的监听;

我们可以将 reactive 函数进行如下的重构:

  • 首先,在传入对象时,我们可以遍历所有的 key,并且通过属性存储描述符来监听属性的获取和修改;
  • 在 setter 和 getter 方法中的逻辑和前面的 Proxy 是一致的;
js
function reactive2(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function () {
        const dep = getDepends(obj, key)
        dep.depend()
        return value
      },
      set: function (newValue) {
        const dep = getDepends(obj, key)
        value = newValue
        dep.notify()
      }
    })
  })
  return obj
}