全面了解Vue3的 reactive 和相关函数
Vue3的 reactive 怎么用,原理是什么,官网上和reactive相关的那些函数又都是做什么用处的?这里会一一解答。
ES6的Proxy
Proxy 是 ES6 提供的一个可以拦截对象基础操作的代理。因为 reactive 采用 Proxy 代理的方式,实现引用类型的响应性,所以我们先看看 Proxy 的基础使用方法,以便于我理解 reactive 的结构。
我们先来定义一个函数,了解一下 Proxy 的基本使用方式:
// 定义一个函数,传入对象原型,然后创建一个Proxy的代理
const myProxy = (_target) => {
// 定义一个 Proxy 的实例
const proxy = new Proxy(_target, {
// 拦截 get 操作
get: function (target, key, receiver) {
console.log(`getting ${key}!`, target[key])
// 用 Reflect 调用原型方法
return Reflect.get(target, key, receiver)
},
// 拦截 set 操作
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 用 Reflect 调用原型方法
return Reflect.set(target, key, value, receiver)
}
})
// 返回实例
return proxy
}
// 使用方法,是不是和reactive有点像?
const testProxy = myProxy({
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
console.log('自己定义的Proxy实例:')
console.log(testProxy)
// 测试拦截情况
testProxy.name = '新的名字' // set操作
console.log(testProxy.name) // get 操作
Proxy 有两个参数 target 和 handle。
* target:要代理的对象,也可以是数组,但是不能是基础类型。
* handler:设置要拦截的操作,这里拦截了 set 和 get 操作,当然还可以拦截其他操作。
我们先来看一下运行结果:
- Handler 可以看到我们写的拦截函数 get 和 set;
- Target 可以看到对象原型。
注意:这里只是实现了 get 和 set 的拦截,并没有实现数据的双向绑定,模板也不会自动更新内容,Vue内部做了很多操作才实现了模板的自动更新功能。
用 Proxy 给 reactive 套个娃,会怎么样?
有个奇怪的地方,既然 Proxy 可以实现对 set 等操作的拦截,那么 reactive 为啥不返回一个可以监听的钩子呢?为啥要用 watch 来实现监听的工作?
为啥会这么想?因为看到了 Vuex4.0 的设计,明明已经把 state 整体自动变成了 reactive 的形式,那么为啥还非得在 mutations 里写函数,实现 set 操作呢?好麻烦的样子。
外部直接对 reactive 进行操作,然后 Vuex 内部监听一下,这样大家不就都省事了吗?要实现插件功能,还是跟踪功能,不都是可以自动实现了嘛。
所以我觉得还是可以套个娃的。
实现模板的自动刷新
本来以为上面那个 myProxy 函数,传入一个 reactive 之后,就可以自动实现更新模板的功能了,结果模板没理我。
这不对呀,我只是监听了一下,不是又交给 reactive 了吗?为啥模板不理我?
经过各种折腾,终于找到了原因,于是函数改成了这样:
/**
* 用 Proxy定义一个 reactive 的套娃,实现可以监听任意属性变化的目的。(不包含嵌套对象的属性)
* @param {*} _target 要拦截的目标
* @param {*} callback 属性变化后的回调函数
*/
const myReactive = (_target, callback) => {
let _change = (key, value) => {console.log('内部函数')}
const proxy = new Proxy(_target, {
get: function (target, key, receiver) {
if (typeof key !== 'symbol') {
console.log(`getting ${key}!`, target[key])
} else {
console.log('getting symbol:', key, target[key])
}
// 调用原型方法
return Reflect.get(target, key, receiver)
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}:${value}!`)
// 源头监听
if (typeof callback === 'function') {
callback(key, value)
}
// 任意位置监听
if (typeof _target.__watch === 'function') {
_change(key, value)
}
// 调用原型方法
return Reflect.set(target, key, value, target) // 这里有变化,最后一个参数改成 target
}
})
// 实现任意位置的监听,
proxy.__watch = (callback) => {
if (typeof callback === 'function') {
_change = callback
}
}
// 返回实例
return proxy
}
代码稍微多了一些,我们一块一块看。
get
这里要做一下 symbol 的判断,否则会报错。好吧,其实我们似乎不需要 console.log。set
这里改了一下最后一个参数,这样模板就可以自己更新了。设置 callback 函数,实现源头监听
设置一个回调函数,才能在拦截到set操作的时候,通知外部的调用者。只是这样只适合于定义实例的地方。那么接收参数的地方怎么办呢?
调用方法如下:
// 定义一个拦截reactive的Proxy
// 并且实现源头的监听
const myProxyReactive = myReactive(retObject,
((key, value) =>{
console.log(`ret外部获得通知:${key}:${value}`)
})
)
这样我们就可以在回调函数里面得到修改的属性名称,以及属性值。
这样我们做状态管理的时候,是不是就不用特意去写 mutations 里面的函数了呢?
- 内部设置一个钩子函数
设置一个 _change() 钩子函数,这样接收参数的地方,可以通过这个钩子来得到变化的通知。
调用方法如下:
// 任意位置的监听
myProxyReactive.__watch((key, value) => {
console.log(`任意位置的监听:${key}:${value}`)
})
只是好像哪里不对的样子。
首先这个钩子没找到合适的地方放,目前放在了原型对象上面,就是说破坏了原型对象的结构,这个似乎会有些影响。
然后,接收参数的地方,不是可以直接得到修改的情况吗?是否还需要做这样的监听?
最后,好像没有 watch 的 deep 监听来的方便,那么问题又来了,为啥 Vuex 不用 watch 呢?或者悄悄的用了?
深层响应式代理:reactive
说了半天,终于进入正题了。
reactive 会返回对象的响应式代理,这种响应式转换是深层的,可以影响所有的嵌套对象。
注意:返回的是 object 的代理,他们的地址是相同的,并没有对object进行clone(克隆),所以修改代理的属性值,也会影响原object的属性值;同时,修改原object的属性值,也会影响reactive返回的代理的属性值,只是代理无法拦截直接对原object的操作,所以模板不会有变化。
这个问题并不明显,因为我们一般不会先定义一个object,然后再套上reactive,而是直接定义一个 reactive,这样也就“不存在”原 object 了,但是我们要了解一下原理。
我们先定义一个 reactive 实例,然后运行看结果。
// js对象
const person = {
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
}
// person 的 reactive 代理 (验证地址是否相同)
const personReactive = reactive(person)
// js 对象 的 reactive 代理 (一般用法)
const objectReactive = reactive({
name: 'jykReactive',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 查看 reactive 实例结构
console.log('reactive', objectReactive )
// 获取嵌套对象属性
const contacts = objectReactive .contacts
// 因为深层响应,所以依然有响应性
console.log('contacts属性:', contacts)
// 获取简单类型的属性
let name = objectReactive.name
// name属性是简单类型的,所以失去响应性
console.log('name属性:', name)
运行结果:
Handler:可以看到 Vue 除重写 set 和 get 外,还重写了deleteProperty、has和ownKeys。
Target: 指向一个Object,这是建立reactive实例时的对象。
属性的结构:
然后再看一下两个属性的打印结果,因为 contacts 属性是嵌套的对象,所以单独拿出来也是具有响应性的。
而 name 属性由于是 string 类型,所以单独拿出来并不会自动获得响应性,如果单独拿出来还想保持响应性的话,可以使用toRef。
注意:如果在模板里面使用{{personReactive.name}}的话,那么也是有响应性的,因为这种用法是获得对象的属性值,可以被Proxy代理拦截,所以并不需要使用toRef。
如果想在模板里面直接使用{{name}}并且要具有响应性,这时才需要使用toRef。
浅层响应式代理:shallowReactive
有的时候,我们并不需要嵌套属性也具有响应性,这时可以使用shallowReactive 来获得浅层的响应式代理,这种方式只拦截自己的属性的操作,不涉及嵌套的对象属性的操作。
const personShallowReactive = shallowReactive({
name: 'jykShallowReactive',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 查看 shallowReactive 实例结构
console.log('shallowReactive', objectShallowReactive)
// 获取嵌套对象属性
const contacts = objectShallowReactive.contacts
// 因为浅层代理,所以没有响应性
console.log('contacts属性:', contacts)
// 获取简单类型的属性
let name = objectShallowReactive.name
// 因为浅层代理且简单类型,所以失去响应性
console.log('name属性:', name)
shallowReactive 也是用 Proxy 实现响应性的,而单独使用contacts属性并没有响应性,因为 shallowReactive 是浅层代理,所以不会让嵌套对象获得响应性。
注意:objectShallowReactive.contacts.QQ = 123 ,这样修改属性也是没有响应性的。
单独使用的属性的形式:
嵌套对象和name属性,都没有变成响应式。
做一个不允许响应的标记:markRaw
有的时候我们不希望js对象变成响应式的,这时我们可以用markRaw 做一个标记,这样即使使用 reactive 也不会变成响应式。
如果确定某些数据是不会变化的,那么也就不用变成响应式,这样可以节省一些不必要的性能开销。
// 标记js对象
const object = markRaw({
name: 'jyk',
age: 18,
contacts: {
QQ: 11111,
phone: 123456789
}
})
// 试图对标记的对象做相应性代理
const retObject2 = reactive(object)
// 使用对象的属性做相应性代理
const retObject1 = reactive({
name: object.name
})
console.log('作为初始值:', retObject1) // 无法变成响应性代理
console.log('无法变成响应式:', retObject2) // 可以变成响应性代理
运行结果:
做标记后的js对象作为参数,不会变成响应式,但是使用属性值作为参数,还是可以变成响应式。
那么哪些地方可以用到呢?我们可以在给组件设置(引用类型的)属性的时候使用,默认情况下组件的属性都是自带响应性的,但是如果父组件里设置给子组件的属性值永远不会发生变化,那么还变成响应式的话,就有点浪费性能的嫌疑了。
如果想节约一下的话,可以在父组件设置属性的时候加上markRaw标记。
深层只读响应式代理:readonly
有的时候虽然我们想得到一个响应式的代理,但是只想被读取,而不希望被修改(比如组件的props,组件内部不希望被修改),那么这时候我们可以用readonly。
readonly可以返回object、reactive或者ref的深层只读代理,我们来分别测试一下:
// object的只读响应代理
const objectReadonly = readonly(person)
// reactive 的只读响应代理
const reactiveReadonly = readonly(objectReactive)
// 查看 readonly 实例结构
console.log('object 的readonly', objectReadonly)
console.log('reactive 的readonly', reactiveReadonly)
// 获取嵌套对象属性
const contacts = reactiveReadonly.contacts
console.log('contacts属性:', contacts) // 因为深层响应,所以依然有响应性
// 获取简单类型的属性
let name = reactiveReadonly.name
console.log('name属性:', name) // 属性是简单类型的,所以失去响应性
运行结果:
- Handler,明显拦截的函数变少了,set的参数也变少了,点进去看源码,也仅仅只有一行返回警告的代码,这样实现拦截设置属性的操作。
- Target,指向object。
运行结果:
- Handler,这部分是一样的。
- Target,指向的不是object,而是一个Proxy代理,也就是reactive。
浅层只读响应代理:shallowReadonly
和readonly相对应,shallowReadonly是浅层的只读响应代理,和readonly的使用方式一样,只是不会限制嵌套对象只读。
// object 的浅层只读代理
const objectShallowReadonly = shallowReadonly(person)
// reactive 的浅层只读代理
const reactiveShallowReadonly = shallowReadonly(objectReactive)
shallowReadonly的结构和 readonly 的一致,就不贴截图了。
获取原型:toRaw
toRaw 可以获取 Vue 建立的代理的原型对象,但是不能获取我们自己定义的Proxy的实例的原型。
toRaw大多是在Vue内部使用,目前只发现在向indexedDB里面写入数据的时候,需要先用 toRaw 取原型,否则会报错。
// 获取reactive、shallowReactive、readonly、shallowReadonly的原型
console.log('深层响应的原型', toRaw(objectReactive))
console.log('浅层响应的原型', toRaw(objectShallowReactive))
console.log('深层只读的原型', toRaw(objectReadonly))
console.log('浅层只读的原型', toRaw(objectShallowReadonly))
运行结果都是普通的object,就不贴截图了。
类型判断
Vue提供了三个用于判断类型的函数:
* isProxy:判断对象是否是Vue建立的Proxy代理,包含reactive、readonly、shallowReactive和shallowReadonly创建的代理,但是不会判断自己写的Proxy代理。
- isReactive:判断是否是reactive创建的代理。如果readonly的原型是reactive,那么也会返回true。
* isReadonly:判断是否是readonly、shallowReadonly创建的代理。这个最简单,只看代理不看target。
我们用这三个函数判断一下我们上面定义的这些Proxy代理,看看结果如何。
我们写点代码对比一下:
const myProxyObject = myProxy({title:'222', __v_isReactive: false})
console.log('myProxyObject', myProxyObject)
const myProxyReactive = myProxy(objectReactive)
console.log('myProxyReactive', myProxyReactive)
// 试一试 __v_isReadonly
console.log('objectReactive', objectReactive)
console.log('__v_isReadonly'
, objectReactive.__v_isReadonly
, objectReactive.__v_isReactive
)
return {
obj: { // js对象
check1: isProxy(person),
check2: isReactive(person),
check3: isReadonly(person)
},
myproxy: { // 自己定义的Proxy object
check1: isProxy(myProxyObject),
check2: isReactive(myProxyObject),
check3: isReadonly(myProxyObject)
},
myproxyReactive: { // 自己定义的Proxy reactive
check1: isProxy(myProxyReactive),
check2: isReactive(myProxyReactive),
check3: isReadonly(myProxyReactive)
},
// 深层响应 reactive(object)
reto: { // reactive(object)
check1: isProxy(objectReactive),
check2: isReactive(objectReactive),
check3: isReadonly(objectReactive)
},
// 浅层响应 参数:object
shallowRetObj: {
check1: isProxy(objectShallowReactive),
check2: isReactive(objectShallowReactive),
check3: isReadonly(objectShallowReactive)
},
// 浅层响应 参数:reactive
shallowRetRet: {
check1: isProxy(objectShallowReactive),
check2: isReactive(objectShallowReactive),
check3: isReadonly(objectShallowReactive)
},
// 深层只读,参数 object =======================
readObj: { // readonly object
check1: isProxy(objectReadonly),
check2: isReactive(objectReadonly),
check3: isReadonly(objectReadonly)
},
// 深层只读,参数 reactive
readRet: { // readonly reactive
check1: isProxy(reactiveReadonly),
check2: isReactive(reactiveReadonly),
check3: isReadonly(reactiveReadonly)
},
// 浅层只读 参数:object
shallowReadObj: {
check1: isProxy(objectShallowReadonly),
check2: isReactive(objectShallowReadonly),
check3: isReadonly(objectShallowReadonly)
},
// 浅层只读 参数:reactive
shallowReadRet: {
check1: isProxy(reactiveShallowReadonly),
check2: isReactive(reactiveShallowReadonly),
check3: isReadonly(reactiveShallowReadonly)
},
person
}
对比结果:
总结一下:
isReadonly 最简单,只有readonly、shallowReadonly建立的代理才会返回 true,其他的都是 false。
isProxy也比较简单,Vue建立的代理才会返回true,如果是自己定义的Proxy,要看原型是谁,如果原型是 reactive(包括其他三个)的话,也会返回true。
isReactive就有点复杂,reactive 建立的代理会返回 true,其他的代理(包含自己写的)还要看一下原型,如果是 reactive 的话,也会返回true。
判断依据
那么这三个函数是依据什么判断的呢?自己做的 Proxy 无意中监控到了“__v_isReactive”,难道是隐藏属性?测试了一下,果然是这样。
myProxy({title:'测试隐藏属性', __v_isReactive: true}),这样定义一个实例,也会返回true。
reactive直接赋值的方法
使用的时候我们会发现一个问题,如果直接给 reactive 的实例赋值的话,就会“失去”响应性,这个并不是因为 reactive 失效了,而是因为 setup 只会运行一次,return也只有一次给模板提供数据(地址)的机会,模板只能得到一开始提供的 reactive 的地址,如果后续直接对 reactive 的实例赋值操作,会覆盖原有的地址,产生一个新的Proxy代理地址,然而模板并不会得到这个新地址,还在使用“旧”地址,因为无法获知新地址的存在,所以模板不会有变化。
那么就不能直接赋值了吗?其实还是有方法的,只需要保证地址不会发生变化即可。
对象的整体赋值的方法。
有请 ES6 的 Object.assign 登场,这个方法是用来合并两个或者多个对象的属性的,如果属性名称相同后面的属性会覆盖前面的属性。所以大家在使用的时候要谨慎使用,确保两个对象的属性就兼容的,不会冲突。
代码如下:
Object.assign(objectReactive, {name: '合并', age: 20, newProp: '新属性'})
数组的整体赋值的方法。
数组就方便多了,可以先清空再 push 的方式,代码如下:
// retArray.length = 0 // 这里清空的话,容易照成闪烁,所以不要急
setTimeout(() => {
const newArray = [
{ name: '11', age: 18 },
{ name: '22', age: 18 }
]
// 等到这里再清空,就不闪烁了。
retArray.length = 0
retArray.push(...newArray)
}, 1000)
var 和 let、const
ES6 新增了 let 和 const,那么我们应该如何选择呢?
简单的说,var不必继续使用了。
let 和 const 的最大区别就是,前者是定义“变量”的,后者是定义“常量”的。
可能你会觉得奇怪,上面的代码都是用const定义的,但是后续代码都是各种改呀,怎么就常量了?其实const判断的是,地址是否改变,只要地址不变就可以。
对于基础类型,值变了地址就变了;而对于引用类型来说,改属性值的话,对象地址是不会发生变化的。
而 const 的这个特点整合可以用于保护 reactive 的实例。由Vue的机制决定,reactive的实例的地址是不可以改变的,变了的话模板就不会自动更新,const可以确保地址不变,变了会报错(开发阶段需要eslint支持)。
于是const和reactive(包括 ref 等)就成了绝配。
源码:
https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi
在线演示:
https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/
全面了解Vue3的 reactive 和相关函数的更多相关文章
- 全面了解Vue3的 ref 和相关函数和计算属性
基础类型的响应性 -- ref 在vue3里面,我们可以通过 reactive 来实现引用类型的响应性,那么基础类型的响应性如何来实现呢? 可能你会想到这样来实现: const count = rea ...
- Vue3响应式系统api 之 ref reactive
reactive 接收一个普通对象然后返回该普调对象的响应式代理.等同于2.x的 Vue.observable() Vue3中响应数据核心是 reactive , reactive 中的实现是由 P ...
- vue3函数setUp和reactive函数详细讲解
1 setUp的执行时机 我们都知道,现在vue3是可以正常去使用methods的. 但是我们却不可以在setUp中去调用methods中的方法. 为什么了??? 我们先了解一下下面这两个生命周期函数 ...
- 认真总结Vue3中ref与reactive区别和isRef与isReactive 类型判断
1.什么是ref? 1.ref和reactive-样 也是用来实现响应式数据的方法 由于reactive必须传递一个对象, 所以导致在企业开发中如果我们只想让某个变量实现响应式的时候会非常麻烦 所以V ...
- Vue3中的响应式对象Reactive源码分析
Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...
- # vue3 ref 和 reactive 函数
vue3 ref 和 reactive 函数 前言 上一篇博文介绍 setup 函数的时候,最后出现一个问题,就是在 setup 函数中,编写一个事件,直接去修改定义的变量,发现页面上没有更新成功,并 ...
- Vue3.0工程创建 && setup、ref、reactive函数 && Vue3.0响应式实现原理
1 # 一.创建Vue3.0工程 2 # 1.使用vue-cli创建 3 # 官方文档: https://cli.vuejs.org/zh/guide/creating-a-project.html# ...
- 第三十九篇:Vue3 watch(ref和reactive的监视)
好家伙, 1.vue2中的watch是调用配置项,(只能写一个) vue3中的watch是一个函数(可以写很多个) 2.watch一些用法: 这里是定义的数据 set up(){ let sum =r ...
- 熬夜讲解vue3组合API中setup、 ref、reactive的用法
1.初识setUp的使用 简单介绍下面的代码功能: 使用ref函数,去使用监听某一个变量的变化,并且把它渲染到视图上. setUp函数是组合API的入口函数.这个是非常重要的. setUp可以去监听变 ...
随机推荐
- TS & ES-Next & playground
TS & ES-Next & playground TS TypeScript: TS Playground - An online editor for exploring Type ...
- js 深入原理讲解系列-Promise
js 深入原理讲解系列-Promise 能看懂这一题你就掌握了 js Promise 的核心原理 不要专业的术语,说人话,讲明白! Q: 输出下面 console.log 的正确的顺序? const ...
- css break-inside
css break-inside The break-inside CSS property sets how page, column, or region breaks should behave ...
- redux & dispatch & payload
redux & dispatch & payload react & react-redux & redux-saga // contrast dispatch({ t ...
- Flutter: The getter 'futureDynamicType' was called on null.
> flutter packages pub upgrade
- 基本ILS面的评估
一.定义与用途 基本ILS面是ICAO DOC8168飞行程序设计规范中提到的一种限制面. 它相当于附件14中代码为3或4的精密进近跑道所规定的障碍物限制面的子集. 包含:进近面(分为两部分).过渡面 ...
- 数据处理_HIVE增量ETL的一种方式
适用场景: 贴源层主表历史数据过大,ETL不涉及历史数据对比或聚合 处理流程: 1.确定一个业务主键字段或物理主键字段 2.确定一个可以判断增量数据范围的字段,这取决于具体的业务场景,一般选用记录的创 ...
- eclipse修改默认的代码注释
在使用Eclipse编写Java代码时,自动生成的注释信息都是默认是使用的当前登录系统用户名,实际上是可以修改的. 选择Window → Preference → Java → Code Style ...
- RabbitMQ之死信队列
1:何为死信队列 死信队列也是一个正常的队列,可以被消费. 但是,死信队列的消息来源于其他队列的转发. 2:如何触发死信队列 1:消息超时 2:队列长度达到极限 3:消息被拒绝消费,并不再重进队列,且 ...
- SpringBoot Admin应用监控搭建
简介 Spring Boot Admin 用于监控基于 Spring Boot 的应用,它是在 Spring Boot Actuator 的基础上提供简洁的可视化 WEB UI. 参考手册地址:htt ...