你知道Reflect和Proxy是如何劫持对象的属性的吗?

592 阅读9分钟

需求:

有个对象,希望监听这个对象的属性被设置获取删除新增的过程

属性描述符方式

属性描述符中的存储属性描述符可以做到

Object.defineProperty(obj, key, {get/set})

其中get/set可以监听对象属性获取或设置

const obj = {
  name: 'zsf',
  age: 18
}

Object.keys(obj).forEach((key) => {
  let value = obj[key]

  Object.defineProperty(obj, key, {
    get: function () {
      console.log(`${ key }属性被获取了`)
      return value
    },

    set: function (newValue) {
      console.log(`${ key }属性被设置值了`)
      value = newValue 
    }    
  })
})

console.log(obj.name)
obj.name = 'hhh'
console.log(obj.name)

但是,这种方式存在比较大缺点

  • Object.defineProperty设计的初衷不是为了监听一个对象中所有属性的,而是定义普通的属性
  • 但是想监听其它操作,如新增、删除属性,Object.defineProperty做不到

Proxy

es6新增,是个类

  • 监听一个对象的操作
  • 创建一个代理对象
  • 该对象的所有操作,都是通过代理对象完成
  • 代理对象可以监听原对象进行了哪些操作

基本使用

  • 第2个参数是捕获器对象,会重写原的对象set和get方法
  • target参数:代理的对象
  • key参数:代理的对象的key
const obj = {
  name: 'zsf',
  age: 18
}

const objProxy = new Proxy(obj, {
  // 获取值时捕获器
  get: function (target, key, receiver) {
    console.log('获取')
    return target[key]
  },
  // 设置值时的捕获器
  set: function (target, key, newValue, receiver) {
    console.log('设置')
    target[key] = newValue
  }
})
objProxy.age = 20
console.log(objProxy.age)

其它捕获器

  • has-in的捕获器
  • deleteProperty捕获器
const obj = {
  name: 'zsf',
  age: 18
}

const objProxy = new Proxy(obj, {
  // has-in捕获器
  has: function (target, key) {
    console.log('存在')
    return key in target
  },
  // deleteProperty捕获器
  deleteProperty: function (target, key) {
    console.log('删除')
    delete target[key]
  }
})

console.log('name' in objProxy)
delete objProxy.age

还有其它捕获器

getPropertyOf()Object.getPropertyOf()的捕获器
setPropertyOf()Object.setPropertyOf()的捕获器
isExtensible()Object.isExtensible()的捕获器
preventExtensions()Object.preventExtensions()的捕获器
getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor()的捕获器
defineProperty()Object.defineProperty()的捕获器

其它就不列啦

Reflect

es6新增,是个对象,反射

这个reflect有什么用呢?

提供很多操作js对象方法,有点像Object操作对象的方法

已经有Object了,为什么还要reflect呢?

  • 早期ECMA规范没有考虑到对对象本身的一些操作如何设计更加规范,所以将这些api都放到Object上
  • 但是Object作为构造函数,这些操作放在它身上并不合适
  • 所以,es6新增了reflect对象,将这些操作集中到reflect身上

具体哪些方法放到reflect,去MDN

常见方法

Proxy有哪些捕获器,Reflect就有哪些方法

基本使用

Proxy出现的本意是不对原对象进行操作,但是这两个操作就是对原对象的操作

  • return target[key]
  • target[key] = newValue

所以需要Reflect对原对象做一个映射,并且有操作对象那些方法

const obj = {
  name: 'zsf',
  age: 18
}

const objProxy = new Proxy(obj, {
  // 获取值时捕获器
  get: function (target, key) {
    return Reflect.get(target, key, receiver)
  },
  // 设置值时的捕获器
  set: function (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
  }
})
objProxy.age = 20
console.log(objProxy.age)

参数receiver的作用

将指向原对象objthis改成指向代理对象objProxy,这样让代理对象变得有意义,不然会直接绕过代理对象去使用原对象

看这段代码

const obj = {
  _name: 'zsf',
  get_name () {
    return this._name
  },
  set_name (newName) {
    this._name = newName
  }
}

const objProxy = new Proxy(obj, {
  get (target, key) {
    return Reflect.get(target, key)
  },
  set (target, key, newValue) {
    Reflect.set(target, key, newValue)
  }
})
console.log(objProxy.name)// hhh
console.log(obj._name)// zsf
  1. 访问objProxy.name
  2. 来到get捕获器里面
  3. get捕获器通过reflect.get(target, key)会去访问obj.get_name()
  4. 注意,可不是访问_name

reflect的construct

应用场景

es6转es5

function Student(name, age) {
  this.name = name
  this.age = age
}

function Teachear() {
  
}

const stu = new Student('zsf', 18)
console.log(stu)

打印出来的stu是Student类型,但是现在有这样一个需求

依然使用Student()创建出stu对象,但是希望是Teacher类型

3个参数:

  • 1构造类型
  • 2参数列表[]
  • 3目标类型
function Student(name, age) {
  this.name = name
  this.age = age
}

function Teachear() {
  
}

const obj = Reflect.construct(Student, ['zsf', 18], Teachear)
console.log(obj)//Teachear {name: 'zsf', age: 18}

响应式

看这个场景

  • m有一个初识值,有一段代码使用了m
  • m有了新的值时,这段代码可以自动重新执行

这就实现了m的响应式

但是,更多的是需要对象的响应式

思路

1.响应式函数的封装

需要响应式的函数(或用到对象属性的代码块)收集起来

function watchFn (fn) {
  //收集进数组
}

watchFn(function () {
  console.log(objProxy.name)
})

2.依赖收集与通知类的封装

class Depend {
  constructor () {
    this.reativeFns = []
  }
  // 收集
  addDepend (reactiveFn) {
    this.reativeFns.push(reactiveFn)
  }
  // 通知
  notify () {
    this.reativeFns.forEach((fn) => {
      fn()
    })
  }
}

const depend = new Depend()

function watchFn (fn) {
  depend.addDepend(fn)
}

3.自动监听对象的变化

  • Proxy(Vue3)
  • Object.definePropety(Vue2)
// 监听对象的属性改变: 
const objProxy = new Proxy(obj, {
  get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    depend.notify()
  }
})

4.依赖收集的管理

每一个对象属性应该对应一个depend对象

还有多个对象

对应关系:

  • 每个属性对应一个depend对象
  • 每个对象对应多个属性

属性与depend的关系用map保存

一个对象:

const obj = {
  name: 'zsf',
  age: 18
}

const objMap = new Map()
objMap.set('name', nameDepend)
objMap.set('age', ageDepend)

const depend = objMap.get('name')
depend.notify()

多个对象:

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

const info = {
  name: 'info',
  size: 18
}

const objMap = new Map()
objMap.set('name', nameDepend)
objMap.set('age', ageDepend)

const infoMap = new Map()
objMap.set('name', nameDepend)
objMap.set('size', sizeDepend)

objMap、infoMap对应的对象的关系再用WeakMap

const targetMap = WeakMap()
target.set(obj, objMap)
target.set(info, infoMap)

这时,当obj.name发生变化

// 取出objMap,再取出nameDepend,然后通知变化
targetMap.get(obj).get('name').notify()

封装一个获取depend函数

const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map
  let map = targetMap.get(target)
  //edge case 第一次创建时map是空的,因为targetMap还没保存过
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  // edge case 第一次创建时map是空的
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }

  return depend
}

如何将需要响应式的函数添加到对应的depend对象里呢?

let activeReactiveFn = null
function watchFn (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

// 监听对象的属性改变: Proxy(Vue3)/Object.definePropety(Vue2)
const objProxy = new Proxy(obj, {
  get (target, key, receiver) {
    const depend = getDepend(target, key)
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 此时收集的依赖是空的
    const depend = getDepend(target, key)
    depend.notify()
  }
})

完整代码

class Depend {
  constructor () {
    this.reativeFns = []
  }
  // 收集
  addDepend (reactiveFn) {
    this.reativeFns.push(reactiveFn)
  }
  // 通知
  notify () {
    this.reativeFns.forEach((fn) => {
      fn()
    })
  }
}

// 封装一个收集需要响应式的函数的函数
const depend = new Depend()
let activeReactiveFn = null
function watchFn (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map
  let map = targetMap.get(target)
  //edge case 第一次创建时map是空的,因为targetMap还没保存过
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  // edge case 第一次创建时map是空的
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }

  return depend
}

// 对象的响应式
const obj = {
  name: 'zsf',
  age: 18
}

const info = {
  name: 'info',
  size: 18
}

// 监听对象的属性改变: Proxy(Vue3)/Object.definePropety(Vue2)
const objProxy = new Proxy(obj, {
  get (target, key, receiver) {
    const depend = getDepend(target, key)
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 此时收集的依赖是空的
    const depend = getDepend(target, key)
    depend.notify()
  }
})

watchFn(function () {
  console.log(objProxy.name)
})

watchFn(function () {
  console.log(objProxy.age)
})

重构依赖收集类

但是还有个问题:同一函数里要是使用了多次同个属性这个函数就会多次放进需要重新执行的数组里,换句话说,就是重复了

将数组换成Set

// 需要收集的响应式函数
let activeReactiveFn = null

class Depend {
  constructor () {
    this.reativeFns = new Set()
  }
  // 收集
  depend () {
    if (activeReactiveFn) {
      this.reativeFns.add(activeReactiveFn)
    }
  }

  // 通知
  notify () {
    this.reativeFns.forEach((fn) => {
      fn()
    })
  }
}

// 封装一个收集需要响应式的函数的函数
const depend = new Depend()

function watchFn (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map
  let map = targetMap.get(target)
  //edge case 第一次创建时map是空的,因为targetMap还没保存过
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  // edge case 第一次创建时map是空的
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }

  return depend
}

// 对象的响应式
const obj = {
  name: 'zsf',
  age: 18
}

const info = {
  name: 'info',
  size: 18
}

// 监听对象的属性改变: Proxy(Vue3)/Object.definePropety(Vue2)
const objProxy = new Proxy(obj, {
  get (target, key, receiver) {
    const depend = getDepend(target, key)

    depend.depend()

    return Reflect.get(target, key, receiver)
  },
  set (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 此时收集的依赖是空的
    const depend = getDepend(target, key)
    depend.notify()
  }
})

watchFn(() => {
  console.log(objProxy.name, '---')
  console.log(objProxy.name, '+++')
})

objProxy.name = 'hhh'

监听多个对象

上面只是针对一个对象实现响应式

新增一个代理对象即可,但是,要是有很多对象需要响应式,就得封装代理对象那部分代码了,并且自动化

function reactive(obj) {
  return new Proxy(obj, {
    get (target, key, receiver) {
      const depend = getDepend(target, key)
  
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // 此时收集的依赖是空的
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

当需要新增响应式对象,只需要这样即可

const objProxy = reactive({
  name: 'zsf',
  age: 18
})

Vue3响应式原理

// 需要收集的响应式函数
let activeReactiveFn = null
// 依赖收集和通知类
class Depend {
  constructor () {
    this.reativeFns = new Set()
  }
  // 收集
  depend () {
    if (activeReactiveFn) {
      this.reativeFns.add(activeReactiveFn)
    }
  }

  // 通知
  notify () {
    this.reativeFns.forEach((fn) => {
      fn()
    })
  }
}

// 封装一个代理对象,并自动化的函数
function reactive(obj) {
  return new Proxy(obj, {
    get (target, key, receiver) {
      const depend = getDepend(target, key)
  
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // 此时收集的依赖是空的
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

// 封装一个收集需要响应式的函数的函数
const depend = new Depend()

function watchFn (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map
  let map = targetMap.get(target)
  //edge case 第一次创建时map是空的,因为targetMap还没保存过
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  // edge case 第一次创建时map是空的
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }

  return depend
}

// 对象的响应式
const fooProxy = reactive({
  name: 'foo'
})

watchFn(() => {
  console.log(fooProxy.name)
})

fooProxy.name = 'hhh'

vue2响应式原理

不是用Proxy和reflect,而是Object.defineProperty

// 需要收集的响应式函数
let activeReactiveFn = null
// 依赖收集和通知类
class Depend {
  constructor () {
    this.reativeFns = new Set()
  }
  // 收集
  depend () {
    if (activeReactiveFn) {
      this.reativeFns.add(activeReactiveFn)
    }
  }

  // 通知
  notify () {
    this.reativeFns.forEach((fn) => {
      fn()
    })
  }
}

// 封装一个代理对象,并自动化的函数
function reactive(obj) {
  Object.keys(obj).forEach((key) => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get () {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set (newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}

// 封装一个收集需要响应式的函数的函数
const depend = new Depend()

function watchFn (fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map
  let map = targetMap.get(target)
  //edge case 第一次创建时map是空的,因为targetMap还没保存过
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  // edge case 第一次创建时map是空的
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }

  return depend
}

// 对象的响应式
const fooProxy = reactive({
  name: 'hhh'
})

watchFn(() => {
  console.log(fooProxy.name)
})

fooProxy.name = 'foo'