【Vue3响应式原理#02】Proxy and Reflect

专栏分享:vue2源码专栏vue3源码专栏vue router源码专栏玩具项目专栏,硬核💪推荐🙌

欢迎各位ITer关注点赞收藏🌸🌸🌸

背景

以下是柏成根据Vue3官方课程整理的响应式书面文档 - 第二节,课程链接在此:Proxy and Reflect - Vue 3 Reactivity | Vue Mastery

本篇文章将解决 上一篇文章 结尾遗留的问题:如何让代码自动实现响应性? 换句话说就是,如何让我们的 effect 自动保存 & 自动重新运行?

上一篇文章 中,我们最终运行的代码长这样

聪明的你会立马发现,我们现在仍要手动调用 track() 来保存 effect;手动调用 trigger() 来运行 effects,这不是脱裤子放屁么

我们想让我们的响应性引擎自动调用 track()trigger()。那么问题就来了,何时才是调用它们的最好时机呢?

从逻辑上来说,如果访问了对象的属性,就是我们调用 track() 去保存 effect 的最佳时机;如果对象的属性改变了,就是我们调用 trigger() 来运行 effects 的最佳时机

所以问题变成了,我们该如何拦截对象属性的访问和赋值操作?

Proxy(代理)

MDN 上的 Proxy 对象是这样定义的

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

也可以理解为在操作目标对象前架设一层代理,将所有本该我们手动编写的程序交由代理来处理,生活中也有许许多多的“proxy”, 如代购,中介,因为他们所有的行为都不会直接触达到目标对象

语法

  • target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

  • handler: 一个通常以函数作为属性的对象,用来定制拦截行为;它包含有 Proxy 的各个捕获器(trap),例如 handler.get() / handler.set()

const p = new Proxy(target, handler)

常用方法

比较常用的两个方法就是 get()set() 方法

方法 描述
handler.get(target, key, ?receiver) 属性读取操作的捕捉器
handler.set(target, key, value, ? receiver) 属性设置操作的捕捉器

handler.get

用于代理目标对象的属性读取操作,其接受三个参数 handler.get(target, propKey, ?receiver)

  • target: 目标对象
  • key: 属性名
  • receiver: Proxy 本身或者继承它的对象,后面会重点介绍

举个栗子

const origin = {}
const obj = new Proxy(origin, {
  get: function (target, key, receiver) {
		return 10
  }
})

obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

在这个栗子中,我们给一个空对象 origin 的 get 架设了一层代理,所有 get 操作都会直接返回我们定制的数字10

需要注意的是,代理只会对 proxy 对象生效,如访问上方的 origin 对象就没有任何效果

handler.set

用于代理目标对象的属性设置操作,其接受四个参数 handler.set(target, key, value, ?receiver)

  • target: 目标对象
  • key: 属性名
  • value: 新属性值
  • receiver: Proxy 本身或者继承它的对象,后面会重点介绍
const obj = new Proxy({}, {
  set: function(target, key, value, receiver) {
    target[key] = value
    console.log('property set: ' + key + ' = ' + value)
    return true
  }
})

'a' in obj  // false
obj.a = 10  // "property set: a = 10"
'a' in obj  // true
obj.a       // 10

Reflect(反射)

MDN 上的 Reflect 对象是这样定义的

Reflect 是一个内建的对象,用来提供方法去拦截 JavaScript的操作。Reflect 不是一个函数对象,所以它是不可构造的,也就是说你不能通过 new操作符去新建一个 Reflect对象或者将 Reflect对象作为一个函数去调用。Reflect的所有属性和方法都是静态的(就像Math对象)

常用方法

Reflect对象挂载了很多静态方法,所谓静态方法,就是和 Math.round() 这样,不需要 new 就可以直接使用的方法。
比较常用的两个方法就是 get()set() 方法:

方法 描述
Reflect.get(target, key, ?receiver) 和 target[key] 类似,从对象中读取属性值
Reflect.set(target, key, value, ? receiver) 和 target[key] = value 类似,给对象的属性设置一个新值

Reflect.get()

Reflect.get方法允许你从一个对象中取属性值,返回值是这个属性值

Reflect.set()

Reflect.set 方法允许你在对象上设置属性,返回值是 Boolean 值,代表是否设置成功

  • target: 目标对象
  • key: 属性名
  • value: 新属性值
  • receiver: 后面会重点介绍
Reflect.get(target, key[, receiver])
// 等同于
target[key]

Reflect.set(target, key, value[, receiver])
// 等同于
target[key] = value

举个栗子

let product = {price: 5, quantity: 2}

// 以下三种方法是等效的
product.quantity
product['quantity']
Reflect.get(product, 'quantity')

// 以下三种方法是等效的
product.quantity = 3
product['quantity'] = 3
Reflect.set(product, 'quantity', 3)

关于receiver参数

在 Proxy 和 Reflect 对象中 get/set() 方法的最后一个参数都是 receiver,它到底是个什么玩意?

receiver 是接受者的意思,译为接收器

  1. 在 Proxy trap 的场景下(例如 handler.get() / handler.set()), receiver 永远指向 Proxy 本身或者继承它的对象,比方说下面这个例子
let origin = { a: 1 }

let p = new Proxy(origin, {
  get(target, key, receiver) {
    return receiver
  },
})

let child = Object.create(p)

p.getReceiver // Proxy {a: 1}
p.getReceiver === p // true
child.getReceiver // {}
child.getReceiver === child // true
  1. 在 Reflect.get / Reflect.set() 的场景下,receiver 可以改变计算属性中 this 的指向
let target = {
  firstName: 'li',
  lastName: 'baicheng',
  get a() {
    return `${this.firstName}-${this.age}`
  },
  set b(val) {
    console.log('>>>this', this)
    this.firstName = val
  },
}

Reflect.get(target, 'a') // li-undefined
Reflect.get(target, 'a', { age: 24 }) // undefined-24

Reflect.set(target, 'b', 'huawei', { age: 24 })
// >>>this {age: 24}
// true

搭配Proxy

在 Proxy 里使用 Reflect,我们会有一个附加参数,称为 receiver (接收器),它将传递到我们的 Reflect调用中。它保证了当我们的对象有继承自其它对象的值或函数时, this 指针能正确的指向对象,这将避免一些我们在 vue2 中有的响应式警告

let origin = { a: 1 }

let p = new Proxy(origin, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  },
})

Reflect对象经常和Proxy代理一起使用,原因有三点:

  1. Reflect提供的所有静态方法和Proxy第2个handle对象中的方法参数是一模一样的,例如Reflect的 get/set() 方法需要的参数就是Proxy get/set() 方法的参数

  2. Proxy get/set() 方法需要的返回值正是Reflect的 get/set() 方法的返回值,可以天然配合使用,比直接对象赋值/获取值要更方便和准确

  3. receiver 参数具有不可替代性!!!

    在下面示例中,我们在页面中访问了 alias 对应的值,稍后 name 变化了,要重新渲染么?

    target[key] 方式访问 proxy.alias 时,获取到 this.name,此时 this 指向 target,无法监控到 name ,不能重新渲染

    Reflect 方式访问 proxy.alias 时,获取到 this.name,此时 this 指向 proxy,可监控到 name ,可以重新渲染

const target = {
  name: '柏成',
  get alias() {
    console.log('this === target', this === target)
    console.log('this === proxy', this === proxy)
    return this.name
  },
}
const proxy = new Proxy(target, {
  get(target, key, receiver) {
    console.log('key:', key)
    return target[key]
    // return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  },
})
proxy.alias

使用 target[key] 打印结果:

使用 Reflect 打印结果:

如何用(How)

让我们创建一个称为 reactive 的函数,如果你使用过Composition API,你会感觉很熟悉。然后再封装一下我们的 handler 方法,让它长得更像 Vue3 的源代码,最后我们将创建一个新的 Proxy对象

代码如下

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
      // 保存effect
      track(target, key)
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        // 运行effect
        trigger(target, key)
      }
      return result
    },
  }
  
  return new Proxy(target, handler)
}

let product = reactive({ price: 5, quantity: 2 })

现在我们已经不再需要手动调用 track()trigger()

让我们分析一下上图内容

  1. 现在我们的响应式函数返回一个 product 对象的代理,我们还有变量 total ,方法 effect()

  2. 当我们运行 effect() ,试图获取 product.price 时,它将运行track(product, 'price')

  3. targetMap 里,它将为 product 对象创建一个新的映射,它的值是一个新的 depsMap ,这将映射 price 属性得到一个新的 dep ,这个 dep就是一个 effects集(Set),把我们 total 的 effect加到这个集(Set)中

  4. 我们还会访问 product.quantity ,这是另一个get请求。我们将会调用track(product, 'quantity')。这将访问我们 product 对象的 depsMap,并添加一个 quantity 属性到一个新的 dep 对象的映射

  5. 然后我们把 total 打印到控制台是 10

  6. 然后我们运行product.quantity = 3,它会调用 trigger(product, 'quantity'),然后运行被存储的所有 effect

  7. 调用 effect() , 就会访问到 product.price ,触发track(product, 'price');访问到 product.quantity ,则触发track(product, 'quantity')

ActiveEffect

我们每访问一次Proxy实例属性,都将会调用一次 track 函数。然后它会去历遍 targetMap、depsMap,以确保当前 effect 会被记录下来,这不合理,不需要多次添加 effect

这不是我们想要的,我们只应该在 effect() 里调用 track 函数

console.log('Update quantity to = '+ product.quantity)
console.log('Update price to = '+ product.price)

为此,我们引入了 activeEffect 变量,它代表现在正在运行中的 effect, Vue3 也是这样做的,代码如下

let activeEffect = null
...
// 负责收集依赖
function effect(eff){ 
  activeEffect = eff 
  activeEffect() // 运行
  activeEffect = null //复位
}

// 我们用这个函数来计算total
effect(() => {
  total = product.price * product.quantity
})

现在我们需要新的 track() 函数,让它去使用这个新的 activeEffect 变量

function track(target, key){
  // 关键!!!
  // 我们只想在我们有activeEffect时运行这段代码
  if(!activeEffect) return

  let depsMap = targetMap.get(target) 
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map())) 
  }
  let dep = depsMap.get(key) 
  if (!dep) {
    depsMap.set(key, (dep = new Set())) 
  }
  //当我们添加依赖(dep)时我们要添加activeEffect
  dep.add(activeEffect)
}

这样就保证了,如果不是通过 effect() 函数去访问Proxy实例属性,则这时的 activeEffect 为 null ,进入 track() 函数立即就被 return 掉了

完整代码

这样一来,我们就实现了 Vue3 基本的响应性了。完整代码如下

// The active effect running
let activeEffect = null

// For storing the dependencies for each reactive object
const targetMap = new WeakMap()

// 负责收集依赖
function effect(eff) {
  activeEffect = eff
  activeEffect() // 运行
  activeEffect = null //复位
}

// Save this code
function track(target, key) {
  // 关键!!!
  // 我们只想在我们有activeEffect时运行这段代码
  if (!activeEffect) return

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  console.log('>>>track', target, key)
  //当我们添加依赖(dep)时我们要添加activeEffect
  dep.add(activeEffect)
}

// Run all the code I've saved
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  let dep = depsMap.get(key)
  if (dep) {
    console.log('>>>trigger', target, key)
    dep.forEach(eff => {
      eff()
    })
  }
}

// 响应式代理
function reactive(target) {
  // 如果不是对象或数组
  // 抛出警告,并返回目标对象
  if (!target || typeof target !== 'object') {
    console.warn(`value cannot be made reactive: ${String(target)}`)
    return target
  }
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver)
      track(target, key)

      // 递归创建并返回
      if (typeof target[key] === 'object' && target[key] !== null) {
        return reactive(target[key])
      }
      return result
    },
    set(target, key, value, receiver) {
      let oldValue = target[key]
      let result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    },
  }
  return new Proxy(target, handler)
}

let product = reactive({ price: 5, quantity: 2, rate: { value: 0.9 } })
let total = 0

effect(() => {
  total = product.price * product.quantity * product.rate.value
})

控制台打印结果如下

参考资料

热门相关:我在末世有套房   法医萌妻,撩上瘾!   年轻的朋友妈妈   高人竟在我身边   法医萌妻,撩上瘾!