JS中的Proxy和Reflect

149 阅读8分钟

Proxy-Reflect vue2-vue3的响应式原理

首先我们先来看一个需求,有一个对象,我们希望监听这个对象中的属性被设置或获取的过程

我们可以通过属性描述符中的存储属性描述符来做到

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

但是我们要知道Object.defineProperty设置的初衷并不是为了去监听一个对象的所有属性的,只是为了定义属性 而且如果我们想监听更加丰富的操作,比如新增属性,删除属性,那么他是无能为力的

Proxy的基本使用

在ES6中新增了Proxy类,用于帮助我们创建一个代理

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

利用proxy实现监听对象的思路

  1. 创建Proxy对象 传入需要监听的对象以及一个处理对象(handler)
  2. 在handler里面进行监听

如果我们想监听某些具体的操作,那么可以在handler中添加对应的捕获器

set和get分别对应的是函数,

set函数有四个参数:

  1. target:目标对象(监听的对象)
  2. property:将被设置的属性key
  3. value:新属性值
  4. receiver:调用的代理对象

get函数有三个参数

  1. target:目标对象(监听的对象)
  2. property:被获取的属性key
  3. receiver:调用的代理对象

receiver的作用是 如果我们的源对象(obj)有setter,getter的访问器属性,那么可以通过receiver来改变里面的this

const obj = {
    name:"why",
    age:18
}
​
const objProxy = new Proxy(obj,{
    // 获取值时的捕获器
    get(target,key){
        console.log(`监听到对象的${key}属性被访问了`,target)
        return target[key]
    }
    
    // 设置值时的捕获器
    set(target,key,newValue){
        console.log(`监听到对象的${key}属性被设置值`,target)
        target[key] = newValue
    }
})
​

其他捕获器:

const obj = {
  name: "why", // 数据属性描述符
  age: 18
}
​
​
const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function(target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target)
    return target[key]
  },
​
  // 设置值时的捕获器
  set: function(target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target)
    target[key] = newValue
  },
​
  // 监听in的捕获器
  has: function(target, key) {
    console.log(`监听到对象的${key}属性in操作`, target)
    return key in target
  },
​
  // 监听delete的捕获器
  deleteProperty: function(target, key) {
    console.log(`监听到对象的${key}属性in操作`, target)
    delete target[key]
  }
})
​
​
// in操作符
// console.log("name" in objProxy)// delete操作
delete objProxy.name

所有的捕获器作用 image.png

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

function foo() {
​
}
​
const fooProxy = new Proxy(foo, {
  apply: function(target, thisArg, argArray) {
    console.log("对foo函数进行了apply调用")
    return target.apply(thisArg, argArray)
  },
  construct: function(target, argArray, newTarget) {
    console.log("对foo函数进行了new调用")
    return new target(...argArray)
  }
})
​
fooProxy.apply({}, ["abc", "cba"])
new fooProxy("abc", "cba")
​

Reflect的基本使用

Reflect也是ES6新增的一个API,他是一个对象,字面的意思是反射

那么这个Reflect有什么用呢?

它主要提供了很多操作JS对象的方法,有点像Object中操作对象的方法

比如: Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf() ; Reflect.defineProperty(target,propertyKey,atttibutes)类似于Object.defineProperty()

如果我们有Object可以做这些操作,那为什么还要有Reflect这样的新增对象呢?

  1. 早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以直接将这些API放在了Object上面
  2. 但是Object作为一个构造函数,这些操作放到他身上并不合适
  3. 另外还包含了类似于in,delete操作符,让JS看起来会有点奇怪
  4. 所以在ES6中新增了Reflect,让我们把操作对象的方法都集中到了Reflect对象上

Reflect的常见方法

image.png 我们可以将之前Proxy案例中对原对象的操作都修改为Reflect来操作

const objProxy = new Proxy(obj,{
    has:function(target,key){
        return Reflect.has(target,key)
    }
    set:function(target,key,value){
        return Reflect.set(target,key,value)
    }
    get:function(target,key){
        return Reflect.get(target,key)
    }
    deleteProperty:function(target,key){
        return Reflect.deleteProperty(target,key)
    }
})

响应式

我们先来看一下响应式意味着什么

  • m有一个初始化的值,有一段代码使用了这个值
  • 那么在m有一个新的值的时候,这段代码可以自动重新执行
let m = 20
console.log(m)
console.log(m*2)
​
m=40

为此我们就需要设计一个响应式函数,当数据变化的时候,自动去执行某一个函数

但是有一个问题:如何区分一个函数是否需要响应式?

我们来封装一个新的函数watchFn,凡是传到这个函数的就是需要响应式的,其他默认定义的函数都是不需要响应式的

原理是当obj对象某个属性的值发生改变的时候,我们就需要调用一个函数,函数就会执行函数体里面的内容,从而实现响应式, 我们将需要执行的函数都放在一个数组里,让对象某个属性的值发生改变的时候,遍历这个数组执行函数方法即可

// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn){
    reactiveFns.push(fn)
}
​
// 对象的响应式
const obj = {
    name:"why",
    age:18
}
​
watchFn(function() {
  const newName = obj.name
  console.log("你好啊, 李银河")
  console.log("Hello World")
  console.log(obj.name) // 100行
})
​
watchFn(function() {
  console.log(obj.name, "demo function -------")
})
​
function bar() {
  console.log("普通的其他函数")
  console.log("这个函数不需要有任何响应式")
}
​
obj.name = "kobe"
reactiveFns.forEach(fn => {
  fn()
})
​

响应式的依赖收集

目前我们收集的以来是放到一个数组中去保存的,但是这样会存在数据管理的问题

  • 我们在开发中需要监听很多对象的响应式
  • 这些对象需要监听的不只是一个属性,他们很多属性的变化,都有对应的响应式函数
  • 我们不可能在全局维护一大堆的数组来保存这些响应函数

所以我们需要设计一个类,这个类用来管理某一个对象的某一个属性的所有响应式函数 相当于替代了原来简单的reactiveFns的数组

class Depend{
    constructor(){
        this.reactiveFns = []
    }
    
    addDepend(reactiveFn){
        this.reactiveFns.push(reactiveFn)
    }
    
    notify(){
        this.reactiveFns.forEach(fn=>{
            fn()
        })
    }
}
​
// 封装一个响应式的函数
const depend = new Depend()
function warchFn(fn){
    depend.addDepend(fn)
}
​
// 对象的响应式
watchFn(function() {
  const newName = obj.name
  console.log("你好啊, 李银河")
  console.log("Hello World")
  console.log(obj.name) // 100行
})
​
watchFn(function() {
  console.log(obj.name, "demo function -------")
})
​
obj.name = "kobe"
depend.notify()

监听对象的变化

我们接下来就可以通过之前的方法来监听对象的变化

  1. 通过Object.defineProperty的方法(vue2)
  2. 通过new Proxy的方法(vue3)

通过proxy方法:

class Depend {
  constructor() {
    this.reactiveFns = []
  }
​
  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }
​
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
​
// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}
​
// 对象的响应式
const obj = {
  name: "why", // depend对象
  age: 18 // depend对象
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    depend.notify()
  }
})
​
watchFn(function() {
  const newName = objProxy.name
  console.log("你好啊, 李银河")
  console.log("Hello World")
  console.log(objProxy.name) // 100行
})
​
watchFn(function() {
  console.log(objProxy.name, "demo function -------")
})
​
watchFn(function() {
  console.log(objProxy.age, "age 发生变化是需要执行的----1")
})
​
watchFn(function() {
  console.log(objProxy.age, "age 发生变化是需要执行的----2")
})
​
objProxy.name = "kobe"
objProxy.name = "james"
objProxy.name = "curry"
​
objProxy.age = 100

对象的依赖管理

我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数

但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理,我们如何可以使用一种数据结构来管理不同对象的不同依赖关系?

我们可以使用ES6中的WeakMap,WeakMap可以让我们在对象上添加属性,并且不干扰垃圾回收机制。

对象的响应式操作vue3

// 保存当前需要收集的响应式函数
let activeReactiveFn = null/**
 * Depend优化:
 *  1> depend方法
 *  2> 使用Set来保存依赖函数, 而不是数组[]
 */class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
​
  // addDepend(reactiveFn) {
  //   this.reactiveFns.add(reactiveFn)
  // }depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
​
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
​
// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}
​
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
​
  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
​
function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key)
      // 给depend对象中添加响应函数
      // depend.addDepend(activeReactiveFn)
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // depend.notify()
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
  name: "why", // depend对象
  age: 18 // depend对象
})
​
const infoProxy = reactive({
  address: "广州市",
  height: 1.88
})
​
watchFn(() => {
  console.log(infoProxy.address)
})
​
infoProxy.address = "北京市"const foo = reactive({
  name: "foo"
})
​
watchFn(() => {
  console.log(foo.name)
})
​
foo.name = "bar"

对象的响应式操作vue2

// 保存当前需要收集的响应式函数
let activeReactiveFn = null/**
 * Depend优化:
 *  1> depend方法
 *  2> 使用Set来保存依赖函数, 而不是数组[]
 */class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
​
  // addDepend(reactiveFn) {
  //   this.reactiveFns.add(reactiveFn)
  // }depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }
​
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}
​
// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}
​
// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
​
  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}
​
function reactive(obj) {
  // {name: "why", age: 18}
  // ES6之前, 使用Object.defineProperty
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set: function(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}
​
// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
  name: "why", // depend对象
  age: 18 // depend对象
})
​
const infoProxy = reactive({
  address: "广州市",
  height: 1.88
})
​
watchFn(() => {
  console.log(infoProxy.address)
})
​
infoProxy.address = "北京市"const foo = reactive({
  name: "foo"
})
​
watchFn(() => {
  console.log(foo.name)
})
​
foo.name = "bar"
foo.name = "hhh"