【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 是接受者的意思,译为接收器
- 在 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
- 在 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代理一起使用,原因有三点:
Reflect提供的所有静态方法和Proxy第2个handle对象中的方法参数是一模一样的,例如Reflect的 get/set() 方法需要的参数就是Proxy get/set() 方法的参数
Proxy get/set() 方法需要的返回值正是Reflect的 get/set() 方法的返回值,可以天然配合使用,比直接对象赋值/获取值要更方便和准确
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()
了
让我们分析一下上图内容
现在我们的响应式函数返回一个 product 对象的代理,我们还有变量 total ,方法 effect()。
当我们运行 effect() ,试图获取 product.price 时,它将运行
track(product, 'price')
在 targetMap 里,它将为 product 对象创建一个新的映射,它的值是一个新的 depsMap ,这将映射 price 属性得到一个新的 dep ,这个 dep就是一个 effects集(Set),把我们 total 的 effect加到这个集(Set)中
我们还会访问 product.quantity ,这是另一个get请求。我们将会调用
track(product, 'quantity')
。这将访问我们 product 对象的 depsMap,并添加一个 quantity 属性到一个新的 dep 对象的映射然后我们把 total 打印到控制台是 10
然后我们运行
product.quantity = 3
,它会调用trigger(product, 'quantity')
,然后运行被存储的所有 effect调用 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的更多相关文章
- 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)
由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...
- vue3响应式原理以及ref和reactive区别还有vue2/3生命周期的对比,第二天
前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...
- vue2响应式原理与vue3响应式原理对比
VUE2.0 核心 对象:通过Object.defineProtytype()对对象的已有属性值的读取和修改进行劫持 数组:通过重写数组更新数组一系列更新元素的方法来实现元素的修改的劫持 Object ...
- 第三十六篇:vue3响应式(关于Proxy代理对象,Reflect反射对象)
好家伙,这个有点难. 1.代理对象Proxy Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找.赋值.枚举.函数调用等). 拦截对象中任意属性的变化,包括:查get, ...
- vue3响应式模式设计原理
vue3响应式模式设计原理 为什么要关系vue3的设计原理?了解vue3构建原理,将有助于开发者更快速上手Vue3:同时可以提高Vue调试技能,可以快速定位错误 1.vue3对比vue2 vue2的原 ...
- vue3 第二天vue响应式原理以及ref和reactive区别
前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...
- vue3剖析:响应式原理——effect
响应式原理 源码目录:https://github.com/vuejs/vue-next/tree/master/packages/reactivity 模块 ref: reactive: compu ...
- 详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
- Vue响应式原理的实现-面试必问
Vue2的数据响应式原理 1.什么是defineProperty? defineProperty是设置对象属性,利用属性里的set和get实现了响应式双向绑定: 语法:Object.definePro ...
- vue2.0与3.0响应式原理机制
vue2.0响应式原理 - defineProperty 这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截 ...
随机推荐
- VMware 备份操作系统
在VMware 中备份方式有两种:快照和克隆. 快照:又称还原点,就是保存在拍快照时系统的状态,包含所有内容.在之后的使用中,随时都可以恢复.[短期备份,需要频繁备份时,使用该方法.操作的虚拟系统一般 ...
- 在Volo.Abp微服务中使用SignalR
假设需要通过SignalR发送消息通知,并在前端接收消息通知的功能 创建SignalR服务 在项目中引用 abp add-package Volo.Abp.AspNetCore.SignalR 在Mo ...
- 服务端apk打包教程
本文我将给大家介绍一个 apk 打包工具 VasDolly 的使用介绍.原理以及如何在服务端接入 VasDolly 进行服务端打渠道包操作. 使用介绍 VasDolly 是一个快速多渠道打包工具,同时 ...
- 行行AI人才直播第15期:【AIGC科技公司法律顾问】Amber《AIGC的法律挑战》
近年来,AIGC技术的迅速进步为社会经济发展带来了新的机遇.各行各业都开始关注AIGC相关技术在商业落地中的应用,AIGC相关的创业及项目如雨后春笋般涌现.然而,AIGC的广泛应用也带来了一系列的法律 ...
- Python实现输入三个整数x,y,z,请把这三个数由小到大输出;
num1=input('请输入第一个数,x:') num2=input('请输入第二个数,y:') num3=input('请输入第三个数,z:') if num1>num2: # if 语句判 ...
- 2023-08-12:用go语言写算法。实验室需要配制一种溶液,现在研究员面前有n种该物质的溶液, 每一种有无限多瓶,第i种的溶液体积为v[i],里面含有w[i]单位的该物质, 研究员每次可以选择一瓶
2023-08-12:用go语言写算法.实验室需要配制一种溶液,现在研究员面前有n种该物质的溶液, 每一种有无限多瓶,第i种的溶液体积为v[i],里面含有w[i]单位的该物质, 研究员每次可以选择一瓶 ...
- 古早wp合集
0x00 首先非常感谢大家阅读我的第一篇.本文章不仅仅是题解,一些细枝末节的小问题也欢迎大家一起解答. 小问题的形式如Qx:xxxxxxx? 欢迎发现小问题并讨论~~ N1nE是本人另外一个名字,目前 ...
- [ABC141E] Who Says a Pun?
2023-02-17 题目 题目传送门 翻译 翻译 难度&重要性(1~10):4 题目来源 AtCoder 题目算法 dp,字符串 解题思路 看到求两个完全相同的子串时,我们可以发现其与求最长 ...
- Anaconda平台下从0到1安装TensorFlow环境详细教程(Windows10+Python)
1.安装Anaconda Anaconda下载链接:Free Download | Anaconda 下载完成之后,开始安装,修改安装路径至指定文件夹下,由于安装过程比较简单,此处略过: 2.Tens ...
- 《Python魔法大冒险》008 石像怪的挑战:运算符之旅
小鱼和魔法师继续深入魔法森林.不久,他们来到了一个巨大的魔法石圈旁边.石圈中心有一个闪闪发光的魔法水晶,周围则是一些神秘的符号.但令人意外的是,水晶的旁边还有一个巨大的石像怪,它的眼睛散发着红色的光芒 ...