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

这篇具有很好参考价值的文章主要介绍了【Vue3响应式原理#02】Proxy and Reflect。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

背景

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

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

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

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

聪明的你会立马发现,我们现在仍要手动调用 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] 打印结果:

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

使用 Reflect 打印结果:

【Vue3响应式原理#02】Proxy and 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()

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

让我们分析一下上图内容

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

控制台打印结果如下

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

参考资料

  • ES6的代理模式 | Proxy | Vue3

  • Proxy是代理,Reflect是干嘛用的? « 张鑫旭-鑫空间-鑫生活

  • Proxy和Reflect中的receiver到底是个什么东西 - 掘金

  • Proxy 和 Reflect 中的 receiver 到底是什么? · Issue #52 · sl1673495/notes文章来源地址https://www.toymoban.com/news/detail-745221.html

到了这里,关于【Vue3响应式原理#02】Proxy and Reflect的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • vue3响应式原理

    Vue 3 中的响应式原理是通过使用 ES6 的 Proxy 对象来实现的。在 Vue 3 中,每个组件都有一个响应式代理对象,当组件中的数据发生变化时,代理对象会立即响应并更新视图。 具体来说,当一个组件被创建时,Vue 会为组件的 data 对象创建一个响应式代理对象。这个代理对象可以

    2024年02月15日
    浏览(61)
  • Vue3响应式原理 私

    响应式的本质:当数据变化后会自动执行某个函数映射到组件,自动触发组件的重新渲染。 响应式的实现方式就是劫持数据,Vue3的reactive就是通过Proxy劫持数据,由于劫持的是整个对象,所以可以检测到任何对象的修改,弥补了2.0的不足。 名词解释: **副作用函数:**函数的

    2024年02月10日
    浏览(31)
  • Vue3 数据响应式原理

    核心: 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等… 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作 Vue3的响应式比Vue2好在哪里? 效率更高了,Vue2中假设监听某个对象,该对象中有一万个属性,他要循

    2024年02月11日
    浏览(40)
  • 手写Vue3响应式数据原理

    我们想要对一个对象数据进行处理,从而实现更改dom。但如何更改对一个对象数据进行更改呢? vue2 的双向数据绑定是利⽤ES5 的⼀个 API ,Object.defineProperty()对数据进⾏劫持 结合 发布订阅模式的⽅式来实现的。 vue3 中使⽤了 ES6 的 ProxyAPI 对数据代理,通过 reactive() 函数给每⼀

    2024年02月11日
    浏览(37)
  • Vue3.0中的响应式原理

    实现原理: - 对象类型:通过 ``Object.defineProperty()``对属性的读取、修改进行拦截(数据劫持)。 - 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。 存在问题: - 新增属性、删除属性, 界面不会更新。 - 直接通过下标修改数组, 界面

    2023年04月17日
    浏览(34)
  • 【Vue3响应式原理#01】Reactivity

    专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核💪推荐🙌 欢迎各位ITer关注点赞收藏🌸🌸🌸 以下是柏成根据Vue3官方课程整理的响应式书面文档 - 第一节,课程链接在此:Vue 3 Reactivity - Vue 3 Reactivity | Vue Mastery,本文档可作为课程的辅助材料,

    2024年02月08日
    浏览(30)
  • 【源码系列#05】Vue3响应式原理(Ref)

    ref: 接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value 可以将 ref 看成 reactive 的一个变形版本,这是由于 reactive 内部采用 Proxy 来实现,而 Proxy 只接受对象作为入参,这才有了 ref 来解决值类型的数据响应, 如果传入 ref 的

    2024年02月03日
    浏览(27)
  • Vue2和Vue3响应式原理实现的核心

    Vue.js 是一个开源的渐进式 JavaScript 前端框架,主要用于构建用户界面和单页应用程序(SPA)。Vue.js 可以轻松地与其他库或现有项目集成使用,并被认为是开发响应式数据驱动的现代 Web 应用的一种有效方式。 Vue.js 的核心特点: 响应式数据绑定:Vue.js 可以通过对数据进行双

    2024年02月08日
    浏览(34)
  • 【手撕源码】vue3响应式原理解析(文末抽奖)

    🐱 个人主页: 不叫猫先生 🙋‍♂️ 作者简介:2022年度博客之星前端领域TOP 2,前端领域优质作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀! 💫优质专栏:vue3从入门到精通、TypeScript从入门到实践 📢 资料领取:前端进阶资料以及文中源

    2024年02月03日
    浏览(27)
  • vue2、vue3、react响应式原理、组件声明周期阐述与对比

    响应式原理: Vue.js 的响应式原理是通过使用 Object.defineProperty 函数来实现的。在 Vue.js 中,当一个对象被传入 Vue 实例的 data 选项中时,Vue.js 会将这个对象的属性转换为 getter 和 setter,以便在属性被访问或修改时能够触发相应的更新。 具体来说,Vue.js 会在实例化过程中递归

    2024年02月06日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包