一个简单示例:
<template>
<div>
<h2>count:</h2>
<h2>person:</h2>
<button @click="count++">count++</button>
<button @click="person.name='李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="changePerson">修改整个人</button>
</div>
</template>
<script setup lang='ts' name='ShallowAPI'>
import { ref } from 'vue'
const count = ref(0)
const person = ref({
name: '张三',
age: 18
})
function changePerson() {
person.value = {
name: 'hahaha',
age: 18
}
}
</script>
如果使用shallowRef
代替ref
定义响应式数据:
import { ref,shallowRef } from 'vue'
const count = shallowRef(0)
const person = shallowRef({
name: '张三',
age: 18
})
就会发现只有直接修改.value
的count++
和修改整个人
按钮是能正常工作的,另外两个按钮不能生效。这是因为与ref
不同,ref
会处理所包含数据的所有层次的响应式,而shallowRef
仅会处理最顶层(.value)的响应式。
使用shallowRef
的好处是因为不用处理多层次的响应式,如果对象包含多个层次的属性,使用shallowRef
可以提升性能。因此如果不关心对象深层次的响应式(比如对象是通过服务端接口获取),就可以使用shallowRef
以提升性能。
ShallowReactive
作用与ShallowRef
类似,是reactive
的替代,同样只处理浅层(第一层属性)响应式。
示例:
<template>
<div>
<h2>person:</h2>
<button @click="person.name='李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.address.city='北京'">修改城市</button>
</div>
</template>
<script setup lang='ts' name='ShallowReactive'>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
</script>
使用shallowReactive
定义响应式数据:
import { shallowReactive } from 'vue'
const person = shallowReactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
此时修改城市
的按钮失效,原因与ShallowRef
相同,这里不再赘述。
Readonly
readonly
用于创建一个响应式数据的只读副本。
示例:
<template>
<div>
<h2>person:</h2>
<button @click="person.name='李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.address.city='北京'">修改城市</button>
</div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue'
const person = reactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
</script>
使用readonly
创建一个只读副本:
<template>
<div>
<h2>person:</h2>
<h2>personReadonly:</h2>
<button @click="person.name = '李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.address.city = '北京'">修改城市</button>
<br></br>
<button @click="personReadonly.name = '李四'">修改副本名字</button>
<button @click="personReadonly.age++">修改副本年龄</button>
<button @click="personReadonly.address.city = '北京'">修改副本城市</button>
</div>
</template>
<script setup lang='ts'>
import { reactive, readonly } from 'vue'
const person = reactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
const personReadonly = readonly(person)
</script>
测试可以发现对副本的修改操作都是不起作用的,并且浏览器控制台会有相应的警告信息:
[Vue warn] Set operation on key "age" failed: target is readonly
需要注意的是,原始的响应式数据与只读副本是有关联的,所以原始数据变化后只读副本的数据同样会改变。
如果你需要确保一个响应式数据不会被其它程序意外修改,可以使用readonly
保护数据。
ShallowReadonly
与shallowRef
类似,shallowReadonly
是readonly
的替代,不过只会限制响应式数据的顶层属性是只读的,深层属性不做限制。
实例:
<template>
<div>
<h2>person:</h2>
<h2>personReadonly:</h2>
<button @click="person.name = '李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.address.city = '北京'">修改城市</button>
<br></br>
<button @click="personReadonly.name = '李四'">修改副本名字</button>
<button @click="personReadonly.age++">修改副本年龄</button>
<button @click="personReadonly.address.city = '北京'">修改副本城市</button>
</div>
</template>
<script setup lang='ts'>
import { reactive, shallowReadonly } from 'vue'
const person = reactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
const personReadonly = shallowReadonly(person)
</script>
这里的修改副本名字
和修改副本年龄
都不起作用,但修改副本城市
正常工作,原因是这是一个深层属性。
ToRaw
使用toRaw
可以获取响应式数据的原始数据(非响应式)。
示例:
import { reactive, toRaw } from 'vue';
const person = reactive({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
const rawPerson = toRaw(person)
console.log("rawPerson", rawPerson)
console.log("person", person)
可以通过浏览器控制台观察到,toRaw
获取到的是响应式数据对应的原始数据(普通对象,非响应式)。
toRaw
的用途是如果需要将数据传给第三方库(比如 Axios),可以使用toRaw
传递一个普通对象,以确保响应式数据不会被第三方库意外修改进而导致视图发生变化所引起的 bug。
MarkRaw
示例:
<template>
<div>
<h2>person:</h2>
<button @click="person.name = '李四'">修改名字</button>
<button @click="person.age++">修改年龄</button>
<button @click="person.address.city = '北京'">修改城市</button>
</div>
</template>
<script setup lang='ts'>
import { reactive } from 'vue';
const rawPerson = {
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
}
const person = reactive(rawPerson)
</script>
示例中使用reactive
创建为普通对象rawPerson
创建了一个响应式数据person
。可以利用这个响应式数据完成视图和数据的双向交互。
可以使用markRaw
标记一个普通对象,让这个普通对象永远无法用于创建响应式数据:
import { reactive, markRaw } from 'vue';
const rawPerson = markRaw({
name: '张三',
age: 18,
address: {
city: '上海',
street: '上海路'
}
})
const person = reactive(rawPerson)
console.log("person", person)
此时所有按钮都会失效,因为reactive(rawPerson)
创建的对象是普通对象,而非响应式对象,这是因为原始对象rawPerson
被markRaw
标记了。
markRaw
的使用场景是如果项目中引入了第三方库的对象,如果这些对象被意外包裹成响应式的,就会导致不必要的性能开销,可以使用markRaw
标记这些对象,以避免这种问题的发生。
CustomRef
使用customRef
可以实现自定义的ref
。,
示例:
<template>
<div>
<h2></h2>
<input v-model="msg">
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue';
const msg = ref('hello world')
</script>
示例使用ref
定义了一个响应式数据,并且在视图中实现了双向绑定。
使用customRef
代替ref
定义响应式数据:
import { customRef } from 'vue';
const msg = customRef((track, trigger) => {
let value = 'hello world'
return {
get() {
track()
console.log('get')
return value
},
set(newValue) {
console.log('set')
value = newValue
trigger()
}
}
})
customRef
接收一个匿名函数作为参数,该函数返回一个包含 Getter 和 Setter 方法的对象。Getter 在响应式数据读取时触发,Setter 在响应式数据修改时触发。此外,匿名函数接收两个参数track
和trigger
,track
函数一般在 Getter 开始时调用,其用途是告诉 vue 这是一个特殊数据,需要持续监视其变化,值改变后视图中需要同步改变。trigger
函数一般在 Setter 结束时调用,其用途是告诉 vue 数据已经发生改变。
此外,自定义响应式数据的真实数据在这里保存在匿名函数的局部变量value
中,Getter 直接返回该值即可,Setter 接收的参数是响应式数据修改后的值,用该值作为value
的新值即可。
使用customRef
可以在原始ref
的基础上进行一些自定义的功能,比如修改数据后视图不立即变化,而是延迟数秒后再改变:
import { customRef } from 'vue';
const msg = customRef((track, trigger) => {
let value = 'hello world'
let timer: number
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger()
}, 1000);
}
}
})
为了方便使用,通常会将自定义 Ref 封装在 hooks 中:
import { customRef } from "vue";
export function useMsgRef(initValue: string, delay: number) {
const msg = customRef((track, trigger) => {
let value = initValue;
let timer: number;
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
return { msg };
}
使用 hooks:
import { useMsgRef } from '@/hooks/useMsgRef';
const { msg } = useMsgRef('hello world', 2000);
Teleport
父组件:
<template>
<div class="outer">
<h2>我是App组件</h2>
<img src="http://www.atguigu.com/images/index_new/logo.png" alt="">
<br>
<Dialog />
</div>
</template>
<script setup lang='ts'>
import Dialog from './Dialog.vue';
</script>
<style scoped>
.outer {
background-color: #ddd;
border-radius: 10px;
padding: 5px;
box-shadow: 0 0 10px;
width: 400px;
height: 400px;
filter: saturate(200%);
}
img {
width: 270px;
}
</style>
子组件:
<template>
<button @click="isShow = true">展示弹窗</button>
<div class="modal2" v-show="isShow">
<h2>我是弹窗的标题</h2>
<p>我是弹窗的内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</template>
<script setup lang='ts'>
import { ref } from 'vue'
const isShow = ref(false)
</script>
<style scoped>
.modal2 {
width: 200px;
height: 180px;
background-color: skyblue;
border-radius: 10px;
padding: 5px;
box-shadow: 0 0 5px;
text-align: center;
position: fixed;
left: 50%;
top: 20px;
margin-left: -100px;
}
</style>
子组件是一个弹框,通常应该基于视窗(Window)进行定位,即显示在整个屏幕的居中位置。但因为这里父组件使用了滤镜(filter: saturate(200%);
),导致子组件的弹窗基于父组件进行定位。
这里可以使用 Teleport(传送门)“强行”改变弹出框的 div 元素在最终 HTML DOM 树中的位置,让其位于 body 节点下,这样就可以基于屏幕进行定位和展示:
<template>
<button @click="isShow = true">展示弹窗</button>
<teleport to="body">
<div class="modal2" v-show="isShow">
<h2>我是弹窗的标题</h2>
<p>我是弹窗的内容</p>
<button @click="isShow = false">关闭弹窗</button>
</div>
</teleport>
</template>
当然,teleport
只会影响最终渲染时的 Html DOM 树结构,不会影响 Vue 中的组件隶属关系。
Suspense
子组件:
<template>
<div class="content2">
<h2>我是Child组件</h2>
<ul>
<li v-for="user in users"></li>
</ul>
</div>
</template>
<script setup lang='ts'>
import axios from 'axios'
const res = await axios.get('https://jsonplaceholder.typicode.com/users')
const users = res.data
console.log(users)
</script>
<style scoped>
.content2 {
background-color: skyblue;
border-radius: 10px;
padding: 10px;
box-shadow: 0 0 10px;
}
</style>
父组件:
<template>
<div class="content">
<h2>我是Suspense组件</h2>
<child />
</div>
</template>
<script setup lang='ts'>
import Child from './Child.vue'
</script>
<style scoped>
.content {
background-color: #ddd;
border-radius: 10px;
padding: 10px;
box-shadow: 0 0 10px;
}
</style>
子组件没有被正常加载,无法在视图中看到子组件。这是因为子组件的setup
方法中有接口调用,且使用await
以同步的方式获取结果,这就会阻止组件的正常创建和加载。
此时可以在父组件中使用suspense
组件异步加载子组件:
<template>
<div class="content">
<h2>我是Suspense组件</h2>
<suspense>
<template #default>
<child />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</suspense>
</div>
</template>
suspense
有两个插槽,default
对应的是异步任务结束后正常加载的内容,fallback
是异步任务执行期间需要展示的内容(菊花图或者加载中之类的信息)。
可以使用浏览器的开发者工具模拟慢速网络下的加载情况,观察
fallback
插槽的作用。如果子组件的异步任务不是以这种方式阻碍
setup
方法(比如将异步任务放在onMounted
钩子中执行),就不需要使用suspense
组件,这里是为了演示suspense
组件的用途。
全局 API
app.component
定义一个组件src\components\Hello.vue
:
<template>
<div>
<h2>Hello World!</h2>
</div>
</template>
<script setup lang='ts'>
</script>
<style scoped></style>
使用app.component
将这个组件注册为全局组件:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import Hello from './components/Hello.vue'
const app = createApp(App)
app.component('Hello', Hello)
app.use(router)
app.mount('#app')
此时该组件可以不需要引入直接使用:
<template>
<div>
<Hello/>
</div>
</template>
<script setup lang='ts'>
</script>
app.config
可以使用app.config
添加全局属性:
app.config.globalProperties.helloMsg = 'Hello World'
此时可以在任意组件的差值表达式中直接使用该属性:
<template>
<div>
<Hello/>
<h2></h2>
</div>
</template>
<script setup lang='ts'>
</script>
缺点是虽然可以正常使用,但是编译器会因为找不到这个变量定义而报错,可以在src\main.ts
中添加以下定义来解决这个问题:
declare module 'vue' {
interface ComponentCustomProperties {
helloMsg: string
}
}
app.directive
使用app.directive
可以定义一个全局指令:
app.directive('beauty',(element,{value})=>{
element.innerText += value
element.style.color = 'green'
element.style.backgroundColor = 'yellow'
})
可以在任意组件中使用这个全局指令:
<h2 v-beauty="123">你好</h2>
文章评论