【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
})

控制台打印结果如下

参考资料

posted @ 2023-11-06 09:10  柏成  阅读(1302)  评论(0)    收藏  举报