文章目录
本次记录vue3相关setup和响应/组合api的使用方法
1 setup
组合api全部都只能在setup中使用,相当于入口。
setup中不能this,因为该函数调用早于data、computed、methods、DOM refs。只能访问:props、attrs、slots、emit
接收参数:setup(props,context){}
。
其中普通js对象context包含字段:const {emit, attrs, slots, expose} = context
,可解构,该context不是当前实例对象(getCurrentInstance()
可以获取当前实例对象)
- attrs和slots为有状态对象,不要解构
- expose用于外部组件实例使用ref时访问本组件的方法或属性。
expose({})
注意:props不能解构,否则失去响应性,可以使用toRefs
转化整个props,或者toRef
转化单独属性
返回值:
- setup返回一个对象,对象属性可以在模板中使用。(返回的对象所有属性会被合并到render函数的context中,所以模板才可以使用)
- setup返回一个函数,该函数必须为渲染函数
<!-- 示例MyBook.vue -->
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 暴露给 template
return {
readersNumber, // 该属性在模板中访问会被自动解包
book
}
}
}
</script>
返回渲染函数:
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 请注意这里我们需要显式使用 ref 的 value,不会解包
return () => h('div', [readersNumber.value, book.title])
}
}
2 reactive
包裹一个对象并返回该对象的响应式副本,基于ES2015的Proxy,与原对象不相等
注意:包裹的对象会进行深度代理
// 包裹普通对象
const obj = reactive({ count: 0 })
// 包裹混合对象,混合对象中的ref对象会被自动解包,此时二者都有响应式
const count = ref(1)
const obj = reactive({ count })
// 也可以后赋值
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
被包裹对象值变化时(指通过回调函数进行变化,如果在setup中直接改变则两者都会导致UI变化),如果模板引用了该对象,则会进行UI更新
如果是普通对象,则UI不会改变
<template>
<h1>{{ state1 }}</h1>
<button @click="change1">1</button>
<h1>{{ state2 }}</h1>
<button @click="change2">1</button>
</template>
<script lang="ts">
import { defineComponent, reactive, shallowReactive } from "vue";
export default defineComponent({
abc: 1,
setup() {
const s1 = { name: "pp", a: { b: { c: 1 } } };
const s2 = { name: "pp", a: { b: { c: 1 } } };
const state1 = reactive(s1);
const state2 = shallowReactive(s2);
const change1 = () => {
state1.a.b.c = 22;
};
const change2 = () => {
state2.a.b.c = 999;
};
// 如果通过此种方式进行改变,则UI会刷新
// setTimeout(() => {
// state1.a.b.c = 2222;
// state2.a.b.c = 333
// }, 2000);
return {
state1,
state2,
change1,
change2,
};
},
});
</script>
2.1 shallowReactive
只转换自身为响应式对象,对于自身包含的属性的值不做转换!即响应一层
与 reactive 不同,任何使用 ref 的 property 都不会被代理自动解包。
3 readonly
包裹一个对象(普通、响应式、ref),返回其只读代理,也是深度的,同样ref会自动解包
const raw = {
count: ref(123)
}
const copy = readonly(raw)
console.log(raw.count.value) // 123
console.log(copy.count) // 123
3.1 shallowReadonly
只转换自生为只读,不嵌套执行转换属性的值
与 readonly 不同,任何使用 ref 的 property 都不会被代理自动解包。
4 isProxy isReactive isReadonly
isProxy:检查对象是否为Proxy代理对象(reactive和readonly包裹的返回都是proxy代理对象)
isReactive:检查对象是否最底层是响应式的(无论之后是否包裹了readonly等)
isReadonly:检查对象是否被readonly包裹过(包过即true)
区分这三者很简单:可以设想reactive包裹后在目标对象上添加了一个隐藏属性A,readonly包裹后会在目标对象上添加隐藏属性B,而这几种是用来判断是否包含对应隐藏属性的。
普通对象->只读->响应:由于被只读包裹后无法添加响应标记,故响应判定false,只读true
普通对象->响应->只读:由于在响应对象上添加新的只读标记,故响应和只读都可检测true
5 toRaw
该api包裹的对象会被解包到创建时的普通js对象,并获取其引用,操作该引用会改变所有对象,但不触发响应的代理副作用。
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
注意:只解包到是一个普通对象即停止!如果该普通对象的某些属性为代理对象,该属性不会被处理
6 markRow
该api用于标记一个普通js对象,表明其不接受转换成proxy对象,返回普通对象本身,标记只存在于第一层!!即无嵌套标记!!
即:被该api标记了的对象通过响应式或只读包裹无效!
跳过无需代理的大对象可以提高性能
不进行嵌套标记意味着:第一层标记为无嵌套对象,但是可以把第二层及更深层包装成响应式,容易混淆,所以尽量避免这种情况发生。
7 Refs相关
7.1 ref
使用ref包裹的基础数据类型会变成响应式,ref 对象仅有一个 .value property,指向该内部值。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
注意:如果ref接收一个基础类型值,则value返回该基础类型值,如果接收的是一个对象,则value返回的是该对象的reactive包裹后的值
7.1.1 模板$refs
属于模板的一个属性:<div ref="a">123</div>
可以定义在普通DOM标签也可以定义在组件标签上
// 模板:<Father ref="father"/>
// 使用:this.$refs.father,可以获取组件实例的一个代理对象
注意:$refs不能在模板中和计算属性中使用
7.1.2 模板refs
在组合式API中即setup中使用模板ref
和普通响应式ref
使用方式一样
<template>
<div ref="root">root</div>
</template>
<script>
...
setup(){
const root = ref(null);
onMounted(()=>console.log(root.value))
return{root}
}
...
</script>
原因是:返回的ref如果和虚拟节点或组件的ref同名,则该节点会被绑定到ref值中,只有在挂载完和更新完才能拿到最新值。
结合v-for
使用ref
<template>
<div v-for="(item,index) of list"
:key="index"
:ref="el=>{if(el) rlist[index]=el}"
>{{item.name}}</div>
</template>
<script>
...
setup(){
const rlist= ref([]);
return{rlist,list:[1,2,3]}
}
...
</script>
7.2 unref
语法糖:val = isRef(val) ? val.value : val
7.3 toRef
从某个响应式对象中创建一个响应式属性
一般应用场景为:响应式的某个属性为基本数据类型,所以按值传递,在处理过程中,无法影响原对象,所以使用toRef创建一个对象,这样该属性就可以按照引用进行传递了
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2 由于传递的是引用,所以改变一个另一个也会变
state.foo++
console.log(fooRef.value) // 3 由于传递的是引用,所以改变一个另一个也会变
7.4 toRefs
将一个响应式对象所有属性全部转成Ref引用,便于解构展开
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
7.5 isRef
判断是否为ref对象
const foo = shallowRef({})
// 改变 ref 的值是响应式的
foo.value = {}
// 但是这个值不会被转换。
isReactive(foo.value) // false
7.6 shallowRef
创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的,只跟踪value引用变化。浅层ref。
const foo = shallowRef({})
// 改变 ref 的值是响应式的
foo.value = {}
// 但是这个值不会被转换。
isReactive(foo.value) // false
此时更改foo的值会触发更新
setup() {
const info = shallowRef({name:1})
watchEffect(()=>{
console.log(info.value.name)
})
info.value={name:2} // 会触发effect
},
此时更改foo值的嵌套属性值不会触发更新
setup() {
const info = shallowRef({name:1})
watchEffect(()=>{
console.log(info.value.name)
})
info.value.name=2 // 不会触发effect 可以使用trigger(info)手动触发
},
7.7 triggerRef
当ref对象作为watchEffect的依赖时,其value的变化会触发effect,当想要手动触发依赖于该ref的effect,可以使用该函数
const shallow = shallowRef({
greet: 'Hello, world'
})
// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这不会触发作用 (effect),因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'
// 记录 "Hello, universe"
triggerRef(shallow)
7.8 customRef
接收两个参数,track和trigger,返回一个对象包括:get和set属性
在取值前调用track,在设置值后调用trigger,以让响应系统工作
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 获取值前通知系统准备跟踪赋值前后数据变化
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 赋值结束调用trigger通知系统
}, delay)
}
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello')
}
}
}
8 computed
接受一个getter函数,并返回一个不可变的响应式ref对象
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
或者接收一个包含get和set属性的对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
9 watchEffect
第一次执行setup函数,就会执行一次
每次依赖被改变也会执行一次
特别注意:对于数组或者对象这种引用数据类型,其中的改变无法被观察到!!!除非显式使用数组的长度或对象的某个变化属性,否则只能使用深拷贝!(可以使用lodash的_.cloneDeep(state)
)或者解构!
9.1 简单使用
自动收集依赖,如果依赖改变则会执行effect,该api返回一个stop函数,执行该函数可以停止该effect的触发
const count = ref(0)
const stop = watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
setTimeout(() => {
stop()
}, 1000)
9.2 清理函数
如需清理effect:在【依赖改变时】或者【组件卸载时】清理副作用,需要传入一个清理注册函数onInvalidate
该函数传入的回调函数执行时机为:
- 依赖改变后,先调用清理在重新执行effect
- 组件卸载前,先执行清理,再卸载
- stop回调前,先执行清理,再停止
使用场景有很多:如input输入框的debounce功能
const rid = ref(id)
watchEffect(async(onInvalidate) => {
// 依赖id改变时需要获取新的数据
const token = await getUserInfo(rid.value)
onInvalidate(() => {
// 在获取新数据时,取消上一次的请求,即清理上一次的作用
token.cancel()
})
})
watchEffect可以直接接收一个异步函数作为回调,而react不可以(需要在内部创建一个异步函数,然后同步调用)
9.3 effect在生命周期中的执行时机
默认行为:watchEffect中的effect会在组件update【之前】被调用!
可选参数
watchEffect(()=>{},{flush:'pre/post/sync'})
// pre 默认模式,effect在组件更新前执行
// post effect在组件更新后执行
// sync 同步模式,即所有effect强制同步,效率低下
同步模式即:
setup() {
const info = shallowRef({name:'pp'})
console.log('1')
watchEffect(()=>{
console.log(info.value.name)
},{flush:'sync'})
console.log('2')
info.value={name:'qq'}
console.log('3')
},
打印输出:
1
pp
2
qq
3
9.4 watchEffect中访问DOM
处于setup中的watchEffect无法访问DOM,因为此时组件还未挂载,如需访问需要放在onMounted
生命周期中
原因:在setup中第一次effect执行时,组件尚未挂载,无法获取有关DOM的数据,如ref。组件挂载完毕,DOM上的ref被赋值,重新触发effect,此时可以获取数据。
9.5 watch debugger
watchEffect接收的第二个参数是一个options对象,包含onTrack
和onTrigger
两个函数,用于追踪依赖和回调触发的调试
watchEffect(
() => {
/* 副作用 */
},
{
onTrack(e) { // 跟踪依赖和依赖改变后重新跟踪
debugger
},
onTrigger(e) { // 依赖改变,回调触发
debugger
}
}
)
10 watch
同watchEffect区别:
1 第一次setup不会执行,依赖改变才会执行
2 明确指定依赖
3 可以获取依赖前后变化
第一个参数:接收一个getter函数,或一个ref
第二个参数:副作用回调
第三个参数:调试options,可以深度观察:deep:true
,解决数组和对象无法被观察到内部改变的问题(也可以深拷贝等)。可以立即执行:immediate:true
,解决首次渲染不执行的问题。
其余行为同watchEffect
一样
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount, onInvalidate) => {
/* ... */
}
)
// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// 侦听多个ref
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
注意多个同步更改只会触发【一次】侦听器。
如果需要同步多次触发更改,有两个方法
- 将
flush:'sync'
,效率低 - 在两次同步更改中间添加异步调用:
await nextTick()
const changeValues = async () => {
firstName.value = 'John' // 打印 ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}
11 provide/inject
对于嵌套多层的组件,想要传递参数,可以使用该api
provide:上层组件提供数据
inject:下层组件使用数据
11.1 选项API用法
// provide 上层组件提供选项
// 静态数据-非响应式
provide: {
user: 'John Doe'
}
// 动态数据-非响应式
provide() {
return {
todoLength: this.todos.length
}
}
// 动态数据-响应式
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
// inject 下层组件使用数据
inject: ['user']
注意:如果provide使用的是【选项式API】,即使传递ref,值依然不是响应式!即接收方数据不会改变!
11.2 组合API用法
组合API每次provide一个值:provide(key,value)
每次inject一个值:inject(key,default)
// 上层组件
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
// 下层组件
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
// 响应式
setup(){
const color = ref("red");
provide('color',color)
return {
color
}
},
注意:使用组合api的provide,只需要传递的数据是响应式的即可
在更改传递值时,【建议】在上层组件定义该值的同时,定义一个更改该值的方法,并传递出去,不建议在子组件中直接更改该值!可以在传递时限定readonly
setup(){
const color = ref("red");
const changeColor = (newColor)=>color=newColor;
provide('color',readonly(color))
provide('changeColor',changeColor)
},