Vue3之响应式
如果有多个文件都直接修改数据的话,我们对于数据的更改是不确定的,所以尽可能的对于响应式的状态的变更都保持在供给方组件中,这样确保状态的声明和变更操作都内聚在同一个组件中。是一个钩子函数,是在组件中使用组合API的入口,当我们在选项式API的组件中使用组合API时需要这个函数,组合API都要写在这个函数内。生成的是一个代理对象,可以理解为有一个对象包裹住这个原对象,这个对象就叫原对象的包装对象,响应
1. 响应式基础
1.1 如何创建一个响应式对象或数组(数组本质上也是对象)呢?(如何让一个普通对象使其具有响应性呢?)
使用 vue 的reactive()
函数。
import {reactive} from 'vue';
//这行代码可以放在<script setup> 或者在 setup() 函数中
const state = reactive({count : 0});
state
就是响应式对象。
1.2 如何使用这个响应式对象呢?
在 <script setup>
中的顶层的导入和变量声明,可在同一模板中直接使用。
<template>
<!-- 直接使用state.count -->
<div>当前的值:{{state.count}}</div>
</template>
<script setup>
import {reactive} from "vue";
const state = reactive({count:0});
//延迟5秒后,count数加1
setTimeout(()=>{
state.count +=1;//直接使用
},5000)
</script>
1.3 如果一个普通对象的某个深层属性也是对象或数组,这个普通对象在生成响应式对象时,这个普通对象的深层对象或数组响应式的吗?
reactive()
创建的响应式对象都是深层的,深层对象或数组也是响应式的,他们的改变都能被监测到。响应式对象内的对象依然是代理。
<template>
<!-- 直接使用state.count -->
<div>当前的值:{{state.count}}</div>
<div>当前的值:{{state.taskInfo.status}}</div>
</template>
<script setup>
import {reactive} from "vue";
const state = reactive({count:0,taskInfo:{status:"未开始"}});
state.taskInfo= {status:"暂停中"};//赋值新对象后,state.taskInfo属性还是响应式的
setTimeout(()=>{
state.count +=1;
state.taskInfo.status ="进行中";
},5000)
</script>
1.4 普通对象属性值改变后,界面上也会同步显示吗?
不会,因为原对象就是一个普通对象,通过reactive()
生成的是一个代理对象,可以理解为有一个对象包裹住这个原对象,这个对象就叫原对象的包装对象,响应性是通过这个包装对象实现的。所以普通对象的属性值改变后不会触发更新。
import {reactive} from "vue";
const obj = {count:0,taskInfo:{status:"未开始"},username:['张三','李四']};
const state = reactive(obj);
//obj.count = 15;如果在这里改值的话 界面上显示的值直接就变成15了 ??
setTimeout(()=>{
obj.count = 5;//改变原始对象的属性值
obj.taskInfo = {status:"暂停中"}
obj.username =['马冬梅']
console.log(obj.count)
console.log(obj.taskInfo)
console.log(obj.username)
console.log(state.count)//响应式对象的值也变了,输出的是5,但是不会触发DOM刷新,界面上显示还是原来的值 0,下同
console.log(state.taskInfo)
console.log(state.username)
},5000)
1.5 reactive()
函数存在一些局限
- 仅对对象类型有效,原始类型无效。
- 我们必须始终保持对改响应式对象的引用。如果赋值为新的,那么将和旧的引用断开响应性连接。
1.6 对于原始类型,我们如何把他们变成响应式呢?
使用ref()
可以创建任何值类型的响应式ref
,ref()
函数会将传入的值包装成一个带有value
属性的ref
对象,value
属性的值就是传入的值。当传入的值是对象,将会调用reactive()
自动转换他的.value
。ref
能让我们创造一种对任意值得引用,并且在不丢失响应性的前提下传递这些引用。这是因为传递的都是这个ref
对象。
import {ref} from "vue";
const btnState = ref(0)
setTimeout(()=>{
btnState.value = 1
},5000)
1.7 如何使用这个ref
对象呢?
在模板中属于顶层属性时就不需要解包,他们会自动解包。如果不是顶层属性则不行。我们可以通过解构把他们变为顶层属性。
<template>
<!-- 必须得加上value 不然显示的是[Obejct Object]1,而不是李四1-->
用户名:{{obj2.username.value+'1'}}
<!-- 这个就不需要加value了 ,因为username本身就是顶级属性了-->
用户名:{{username+'1'}}
</template>
<script setup>
import {ref} from "vue";
const obj2 = {username:ref('张三')}
setTimeout(()=>{
obj2.username.value = '李四'
},5000)
const {username} = obj2;
</script>
当这个ref
对象在深层响应式对象中,作为属性被访问时也会自动解包,就像普通对象属性一样。但是浅层响应式对象则不行。
<template>
<!-- 这个就不需要加value了 ,因为是在深层响应式对象中的一个属性,自动解包-->
年龄:{{obj3.age+10}}
</template>
<script setup>
import {ref} from "vue";
const obj3 = reactive({age:ref(20)})
setTimeout(()=>{
obj3.age = 50;//不用加value 自动解包
},5000)
</script>
当ref
对象是响应式数组的一个元素时,使用这个ref
对象也不会自动解包,需要加上value
1.8 不是所有的情况下都会自动解包,每个ref
对象要加一个value
会很麻烦。我们可以使用语法糖来替换。
将ref()改为$ref()即可。
const btnState = $ref(0)
const obj2 = {username:$ref('张三')}
要使用语法糖,需要显示启用。
vite.config.js
需要 “@vitejs/plugin-vue”>=“^2.0.0”,
export default defineConfig({
plugins: [
vue({
reactivityTransform:true,//显示启用语法糖,配置完之后要重启
}),
]
});
2.响应式进阶–组合式API
2.1 setup()
基本使用
setup()
是一个钩子函数,是在组件中使用组合API的入口,当我们在选项式API的组件中使用组合API时需要这个函数,组合API都要写在这个函数内。我们在这个函数内使用组合API声明响应式状态,比如使用ref()
、reactive()
等函数,然后在这个函数返回的对象会暴露这些响应式状态给模板和组件实例。其他的选项可以通过组件实例访问了。
<script>
import {ref} from 'vue';
export default {
setup(){
const username = ref('张三');
return {
username//在调用这个属性不用加value,因为在暴露的时候自动解包了
}
},
methods:{
getName(){
console.log(this.username);//使用this就可以直接调用暴露的属性,
return this.username;
}
},
mounted(){
this.getName();
}
}
</script>
<template>
<div>{{username}}</div>
</template>
setup()
函数有参数 ,第一个参数是组件的props
。和标准组件一致,这个props
也是响应式的,在传入新的props
时会同步更新。如果解构了props
对象,那么解构出的变量就会失去响应性,所以推荐使用props.xxx
的形式使用props
.
export default{
props:{
username,
},
setup(props){
console.log(props.username);
}
}
如果需要解构props
,或者将某个prop
传到外部函数并保持响应性,可以使用toRefs()
或toRef()
函数。
export default{
props:{
username,
},
setup(props){
const {username} = toRefs(props);
//username 是一个ref对象,value是props.username
}
}
第二个参数是一个Setup上下文对象,这个对象暴露了一些可能在setup
中用到的值。上下文对象是非响应式的,可以解构。attrs
和 slots
都是有状态的对象,它们总是会随着组件自身的更新而更新。attrs
和 slots
的属性都不是响应式的。
export default {
setup(props, context) {
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
}
}
2.2 组合式API核心函数
2.2.1 ref()
- 参数:所有值类型,如果传的参数是一个对象,那么这个对象将通过
reactive()
转为具有深层次响应式的对象。 - 返回值:响应式的
ref
对象,此对象内部只有指向内布置的属性value
。所有对value
的操作都将被追踪。 - 语法糖: $ref()
- 例子:可以参照前面举的例子。
2.2.2 computed()
- 参数:
- 接收一个
getter
函数,返回一个只读的响应式ref
对象。 - 接收一个带有
get
和set
函数的对象,返回一个可写的ref
对象。
- 接收一个
- 返回值:返回一个
ref
对象,根据不同参数返回只读或者可写的ref
对象。该ref
通过属性value
暴露getter
函数的返回值。 - 语法糖:$computed()
- 例子
<template>
<div>One的值:{{comCountOne}}</div>
<div>Two的值:{{comCountTwo}}</div>
</template>
<script setup>
import {computed} from "vue";
let count = $ref(0)
//传入一个getter函数 ,返回一个只读的ref对象
const comCountOne = computed(()=>{
return count++;
})
//传入一个带有get和set的对象,返回一个可写的ref对象
const comCountTwo = computed({
get(){
return count
},
set(newVale){
count=newVale + 5;
}
})
setTimeout(()=>{
comCountTwo.value = 100
},5000)
</script>
2.2.3 reactive()
- 参数:一个对象
- 返回值:一个对象的响应式代理对象
- 例子:可以参照前面举的例子。
2.2.4 readonly()
- 参数:接受一个对象,无论是普通对象还是
ref
对象 - 返回值:返回一个原值的只读代理,只读代理是深层的,任何嵌套属性的访问都将是只读的。
2.2.5 watchEffect()
- 参数:
- 第一个参数是要运行的副作用函数。这个副作用函数的参数也是一个函数,这个函数用来注册清理回调。清理回调会在该副作用函数下一次执行钱被调用,可以用来清理无效的副作用。
- 第二个参数是可选的选项。用来调整副作用函数的刷新时机或调试副作用的依赖。默认情况下,侦听器在组件渲染之前执行,设置
flush:'post'
将会是侦听器延迟到组件渲染之后再执行(watchPostEffect()
函数)。在某些特殊情况下可能需要响应式依赖发生改变时立即触发侦听器,这可以通过flush:'sync'
来实现(watchSyncEffect()
函数)。
- 返回值:返回一个用来停止该副作用的函数。
- 例子:
import {watchEffect} from "vue";
let count = $ref(0)
watchEffect(()=>{
console.log(count);
})
count++;
2.2.6 watch()
- 参数:
- 第一个参数,一个或多个响应式数据源,在数据源变化时会调用所给的回调函数。
watch()
是懒监听,只有当数据源变化时才执行回调函数。- 一个函数,返回一个值
- 一个
ref
- 一个响应式对象
- 由以上类型组成的数组
- 第二个参数,发生变化时要调用的回调函数,这个函数有三个参数:新值,旧值,以及一个用于注册副作用清理的回调函数,可以清楚无效的副作用。当监听多个来源时,回调函数接收两个数组,分别对应源数组中的新值和旧值。
- 第三个参数,是可选的,一个对象,有以下选项:
immediate
: 在侦听器创建时立即触发回调。第一次调用时旧值是undefined
deep
: 如果源是对象,强制深度遍历,一遍在深层级变更时触发回调。flush
: 调整回调函数的刷新时机。onTrack
、onTrigger
:调试侦听器的依赖。
- 第一个参数,一个或多个响应式数据源,在数据源变化时会调用所给的回调函数。
- 返回值:返回一个用来停止该副作用的函数。
import {watch,ref,reactive} from "vue";
let one = ref(0)//用$ref()创建的ref对象无法使用watch
const state = reactive({count:0})
//监听ref对象
watch(one,(val,oldVal)=>{
console.log("--"+val)
console.log("--"+oldVal)
})
//一个getter函数
watch(()=>{return state.count},(count,oldCount)=>{
console.log(count)
console.log(oldCount)
})
//监听多个来源
watch([one,state],([newValOne,newState],[old1,old2])=>{
console.log("-newValOne-"+newValOne)
console.log("-newState-"+newState.count)
console.log("-old1-"+old1)
console.log("-old2-"+old2.count)
})
setTimeout(()=>{
one.value = 100;
state.count = 1000;
},5000)
2.3 组合API工具函数
2.3.1 isRef()
- 参数:要检查的值,判断这个值是否为
ref
对象 - 返回值:
true
代表是,否则不是
2.3.2 unref()
- 参数:值
- 返回值:如果参数是
ref
对象,则返回内部值,否则返回参数本身。
2.3.3 toRef()
基于响应式对象上的一个属性,创建一个对应的ref
对象。这样创建的ref
与其源属性保持同步。
- 参数:
- 第一个参数,响应式对象
- 第二个参数,响应式对象的属性名称
- 返回值:
ref
对象 - 语法糖:
$toRef()
,提示未声明? - 例子:
<template>
<div>addressRef的值:{{addressRef}}</div>
<div>user.address的值:{{user.address}}</div>
</template>
<script setup>
import {watch,ref,reactive,toRef} from "vue";
const user = reactive({
username:'张三',
address:'长春'
})
const addressRef = toRef(user,'address')
setTimeout(()=>{
user.address = "松原"
},5000)
setTimeout(()=>{
addressRef.value = "白山"
},10000)
</script>
2.3.4 toRefs()
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的ref
对象。每个ref
都是使用toRef()
创建的。toRefs()
在调用时只会为源对象上可以枚举的属性创建ref
,对可能不存在的不会。
- 参数:响应式对象
- 返回值:普通对象
- 例子:
const user = reactive({
username:'张三',
address:'长春'
})
const userOfRefs = toRefs(user);
const {usernameRef,addressRef} = userOfRefs;//解构也不会丢失响应性
2.3.5 isProxy()
检查一个对象是否有reactive()
、readonly()
、shallowReactive()
、shallowReadonly()
创建的代理
2.3.6 isReactive()
检查一个对象是否有reactive()
、shallowReactive()
创建的代理
2.3.7 isReadonly()
检查一个对象是否有readonly()
、shallowReadonly()
创建的代理
2.4 组合式API–依赖注入
2.4.1 provide()
提供一个值,可以被后代组件注入。provide()
要和setup()
同步调用。
- 参数:
- 第一个参数,要注入的key,可以是一个字符串或一个
symbol
- 第二个参数,是要注入的值。这个值可以是
ref
对象,后代组价可以由此和提供者建立响应式的联系。
- 第一个参数,要注入的key,可以是一个字符串或一个
- 返回值:无
- 例子:
import {provide, reactive,ref} from 'vue'
const obj = reactive({
user:{
username:'张三',
address:'长春'
},
task:{
completeNumber:0,
craft:{
version:1
}
}
})
provide('obj',obj)
const countRef = ref(0)
provide('count',countRef)
setTimeout(()=>{
countRef.value = 1000;
obj.user.address='成都'
obj.task.completeNumber = 100
obj.task.craft.version = 2.4
},5000)
2.4.2 应用层Provide
vue实例也有一个provide()
函数,此函数提供的数据在该应用内的所有组件中都可以注入。
2.4.3 inject()
在当前组件注入上层组件提供的数据。inject()
要和setup()
同步调用。
- 参数:
- 第一个参数,key值。就是祖先组件提供的数据的key。如果祖先组件上有多个组件对同一个key提供了值,那么近的组件会覆盖远的组件提供的值,如果没有会返回
undefined
或默认值。 - 第二个参数,默认值。
- 如果默认值是一个函数,那么需要传第三个参数,这个参数传的值为
false
- 第一个参数,key值。就是祖先组件提供的数据的key。如果祖先组件上有多个组件对同一个key提供了值,那么近的组件会覆盖远的组件提供的值,如果没有会返回
- 返回值:返回祖先组件提供的数据。
- 例子:
<template>
<div>{{countRef}}</div>
<div>地址:{{obj.user.address}}</div>
<div>数量:{{obj.task.completeNumber + 10 }}</div>
<div>版本:{{ obj.task.craft.version}}</div>
</template>
<script setup>
import {inject} from "vue";
const obj = inject('obj')
const countRef = inject('count')
setTimeout(()=>{
countRef.value = 50000;
obj.user.address='武汉'
obj.task.completeNumber = 700
obj.task.craft.version = 12.5
},20000)
</script>
2.4.4 和响应式数据配合使用
在上面的例子中,我们可以看到,有两个setTimeout()
函数都直接修改响应式数据了。如果有多个文件都直接修改数据的话,我们对于数据的更改是不确定的,所以尽可能的对于响应式的状态的变更都保持在供给方组件中,这样确保状态的声明和变更操作都内聚在同一个组件中。我们把上面的例子改一改。
根组件:
<script setup>
import {provide, reactive,ref,readonly} from 'vue'
const obj = reactive({
user:{
username:'张三',
address:'长春'
},
task:{
completeNumber:0,
craft:{
version:1
}
}
})
provide('obj',readonly(obj))//用readonly包装,这样后代组件就不能直接修改了
let countRef = ref(0)
provide('count',readonly(countRef))
setTimeout(()=>{
countRef.value = 1000;
obj.user.address='成都'
obj.task.completeNumber = 100
obj.task.craft.version = 2.4
},5000)
function changeData() {
countRef.value = 50000;
obj.user.address='武汉'
obj.task.completeNumber = 700
obj.task.craft.version = 12.5
}
provide('changeData',changeData)//把这个函数提供给后代组件,后代都通过这个方法进行数据更新
</script>
后代组件
const obj = inject('obj')
const countRef = inject('count')
const changeData = inject('changeData')
setTimeout(()=>{
changeData();//调用祖先组件提供更改数据的方法
},20000)
2.4.5 使用Symbol作注入名
使用字符串作为注入名,可能会引起冲突,使用Symbol
则可以避免这些冲突。
更多推荐
所有评论(0)