文章目录
- 1. ref全家桶
- 1.1 ref()
- 1.2 isRef()以及isProxy()
- 1.3 shallowRef()
- 1.4 triggerRef()
- 1.5 customRef()
- 1.6 unref()
- 2. reactive全家桶
- 2.1 reactive()
- 2.2 readonly()
- 2.3 shallowReactive() 和 shallowReadonly()
- 3. to系列全家桶
- 3.1 toRef()
- 3.2 toRefs()
- 3.3 toRaw()
- 4. computed计算属性
- 案例:购物车总价
- 5. watch监听属性
- 5.1 watch()
- 5.2 watchEffect()
- 5.3 总结
- 6. 组件
- 6.1 组件的生命周期
- 6.2 全局组件的注册以及批量注册
- 6.3 defineProps(父给子传值)
- 响应性语法糖
- 6.4 defineEmits(子给父传值)
- 6.5 defineExpose
- 6.6 递归组件
- 6.7 动态组件
- markRaw
- 7. 插槽
- 7.1 匿名插槽
- 7.2 具名插槽
- 7.3 动态插槽
- 7.4 作用域插槽
- 8. 内置组件
- 8.1 异步组件&代码分包&suspense
- 顶层 await
- 异步组件以及defineAsyncComponent()方法
- Suspense
- 案例可见:vue3笔记案例——Suspense使用之骨架屏
- 8.2 Transition&TransitionGroup动画组件
- 8.3 Teleport传送组件
- 基本使用
- 案例可见:vue3笔记案例——Teleport使用之模态框
- 禁用 Teleport
- 8.4 KeepAlive缓存组件
- 包含/排除(include/exclude)
- 最大缓存实例数(max)
- 缓存实例的生命周期
- 案例:
- 9. 依赖注入(Provide/Inject)
- 9.1 Provide(提供)
- 9.2 Inject (注入)
- 注入默认值
- 和响应式数据配合使用
- 案例
- 9.3 使用 Symbol 作注入名
- 10. 兄弟组件传参以及Mitt
- 10.1 event-bus
- 10.2 Mitt
- 11. TSX
- 12. v-model
- 12.1 组件中的v-model
- 案例:v-model的实现
- 12.2 内置修饰符
- .lazy
- .number
- .trim
- 12.3 自定义修饰符Modifiers
- 基本使用
- 13. 全局API
- 13.1 app.config.globalProperties
- 13.2 nextTick()
- EventLoop
- 14. 自定义指令
- 14.1 指令钩子
- 钩子参数
- 14.2 简写形式
- 案例:简单实现权限指令dome
- 案例:自定义指令实现拖拽效果
- 15. 组合式函数——“vue的hooks”
- 15.1 基本使用
- 命名
- 输入参数
- 返回值
- 16. 插件
- 17. 样式穿透及CSS 新特性
- 18. h函数
- 案例
- 19. 环境变量及proxy代理
- 19.1 环境变量
- 19.2 proxy代理
1. ref全家桶
1.1 ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
type M = {
name: string,
}
const msg1: Ref<string> = ref('字符串')
// const msg1 = ref<string>('字符串')
const msg2 = ref<M>({name: '多多'})
const changeMsg = () =>{
msg1.value = '已修改'
msg2.value.name = '小多改变了'
}
ref
也可以获取dom
属性
<div ref="dom">dom内容</div>
// 名字要与ref绑定的名字一样
const dom = ref<HTMLElement | null>(null)
const changeMsg = () => {
console.log('dom.value?.innerText :>> ', dom.value?.innerText);
console.log('dom :>> ', dom)
}
1.2 isRef()以及isProxy()
- isRef:检查某个值是否为
ref
。 - isProxy:检查一个对象是否是由
reactive()
、readonly()
、shallowReactive()
或shallowReadonly()
创建的代理。
1.3 shallowRef()
ref()
的浅层作用形式。
type M = {
name: string,
}
const msg2 = shallowRef<M>({name: '多多'})
const changeMsg = () =>{
// msg2.value.name = '小多改变了' // 视图不会改变
msg2.value = {
name: '改变了'
}
}
1.4 triggerRef()
强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。强制更新
注意: ref()和shallowRef()不能一块写,不然会影响shallowRef 造成视图更新
const msg1 = ref('字符串')
const msg2 = shallowRef({name: '多多'})
const changeMsg = () =>{
msg1.value = '改变了'
msg2.value.name = '小多改变了,被影响' // 视图也会改变
}
由于 ref底层调用了triggerRef(),所以会造成视图的强制更新
const msg2 = shallowRef({name: '多多'})
const changeMsg = () =>{
msg2.value.name = '小多改变了'
triggerRef(msg2) // 视图强制更新了
}
1.5 customRef()
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
主要应用是:防抖
import { customRef } from 'vue'
function MyRef<T>(value: T, delay = 500) {
let timer: any
return customRef((track, trigger) => {
return {
get() {
track() /* 收集依赖 */
return value
},
set(newVal) {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('触发了');
value = newVal
timer = null
trigger() /* 触发依赖,视图更新 */
}, delay)
},
}
})
}
const msg1 = MyRef<string>('字符串')
// const msg1 = ref<string>('字符串')
const msg2 = MyRef({ name: '多多' })
const changeMsg = () => {
// msg1.value = '小多改变了'
msg2.value = {
name: '改变'
}
}
1.6 unref()
如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
类型
function unref<T>(ref: T | Ref<T>): T
示例
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x)
// unwrapped 现在保证为 number 类型
}
2. reactive全家桶
2.1 reactive()
返回一个对象的响应式代理。
interface Msg = {
name: string
}
// ref 支持所有类型,reactive 只支持引用类型 Array Object Map Set...
// ref 取值赋值都需要添加.value reactive 不需要添加.value
const msg1 = ref({name: 'ref---多多'})
const msg2:Msg = reactive({ name: 'reactive---多多' })
// 不推荐
// const msg2 = reactive<Msg>({ name: 'reactive---多多' })
const changeMsg = () => {
msg1.value.name = 'ref---小多'
msg2.name = 'reactive---小多'
}
reactive proxy
不能直接赋值,否则会破坏响应式对象- 不推荐使用
reactive()
的泛型参数,因为处理了深层次ref
解包的返回值与泛型参数的类型不同。
解决方案:1. 数组可以使用push加解构
<template>
<el-button @click="add">添加</el-button>
<hr class="mtb20" />
<ul>
<li :key="index" v-for="(item, index) in list">{{ item.name }}</li>
</ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
type List = {
name: string
}
let list: List[] = reactive([])
const add = () => {
// 模拟后端获取数据
setTimeout(() => {
let res: List[] = [
{ name: '多多' },
{ name: '小多' },
{ name: '凡凡' },
{ name: '小凡' },
]
list.push(...res)
}, 1000)
}
</script>
<style lang="less" scoped></style>
解决方案: 2. 变成一个对象,把数组作为一个属性去解决
<template>
<el-button @click="add">添加</el-button>
<hr class="mtb20" />
<ul>
<li :key="index" v-for="(item, index) in list.arr">{{ item.name }}</li>
</ul>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
type List = {
name: string
}
let list: {arr: List[]} = reactive({
arr: []
})
const add = () => {
// 模拟后端获取数据
setTimeout(() => {
let res: List[] = [
{ name: '多多' },
{ name: '小多' },
{ name: '凡凡' },
{ name: '小凡' },
]
list.arr = res
}, 1000)
</script>
<style lang="less" scoped></style>
2.2 readonly()
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
// readonly 无法更改只读, 但会受原始数据的影响,原始数据改变则相应改变
let msg1 = reactive({ name: '改变' })
const change = () => {
let copy = readonly(msg1)
msg1.name = '1111'
// copy.name = '2222' // 无法更改
console.log('msg1,copy :>> ', msg1, copy)
}
2.3 shallowReactive() 和 shallowReadonly()
shallowReactive
:reactive()
的浅层作用形式shallowReadonly
:readonly()
的浅层作用形式
3. to系列全家桶
只对响应式对象有效果,对普通对象无效
3.1 toRef()
基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
let msg1 = reactive({ name: '多多', age: 18 })
let age = toRef(msg1, 'age')
const edit = () => {
age.value++
}
应用场景: useDemo(value) 需要一个属性,但定义的是对象,则可以单独把属性取出来使用,而不破坏属性的响应性
3.2 toRefs()
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
let msg1 = reactive({ name: '多多', age: 18 })
// toRefs源码类似
const myTORefs = <T extends object>(object: T) => {
const map: any = {}
for (const key in object) {
map[key] = toRef(object, key)
}
return map
}
// let { name, age } = msg1 /* 直接解构 不具备响应性,更改不会造成视图更新 */
let { name, age } = toRefs(msg1) /* 使其解构的属性具备响应性 */
const edit = () => {
name.value = '小多'
age.value++
}
3.3 toRaw()
根据一个 Vue 创建的代理返回其原始对象。
console.log('msg1, toRaw(msg1) :>> ', msg1, toRaw(msg1));
4. computed计算属性
计算属性就是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
- 函数形式
let price ref<number>(0)
let m = computed<string>(()=>{
return `$` + price.value
})
- 对象形式
let price = ref<number | string>(1)//$0
let mul = computed({
get: () => {
return price.value
},
set: (value) => {
price.value = 'set' + value
}
})
案例:购物车总价
<template>
<table>
<thead>
<tr>
<th align="center">名称</th>
<th align="center">数量</th>
<th align="center">价格</th>
<th align="center">操作</th>
</tr>
</thead>
<tbody>
<tr :key="index" v-for="(item, index) in shop">
<td align="center">{{ item.name }}</td>
<td align="center">
<button @click="addOrSub(item, false)">-</button> {{ item.num }}
<button @click="addOrSub(item, true)">+</button>
</td>
<td align="center">{{ item.price * item.num }}</td>
<td align="center"><button @click="del(index)">删除</button></td>
</tr>
</tbody>
<tfoot>
<td></td>
<td></td>
<td></td>
<td>总价:{{ $total }}</td>
</tfoot>
</table>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
type Shop = {
name: string
price: number
num: number
}
const shop = reactive<Shop[]>([
{
name: '苹果',
price: 10,
num: 1,
},
{
name: '蛋糕',
price: 20,
num: 1,
},
{
name: '面包',
price: 5,
num: 1,
},
])
let $total = ref<number>(0)
$total = computed<number>(() => {
return shop.reduce((prev, next) => {
return prev + next.num * next.price
}, 0)
})
const addOrSub = (item: Shop, flag: boolean): void => {
if (item.num > 0 && !flag) {
item.num--
}
if (item.num < 99 && flag) {
item.num++
}
}
const del = (index: number) => {
shop.splice(index, 1)
}
</script>
<style lang="less" scoped>
table,
tr,
td,
th {
border: 1px solid #ccc;
padding: 20px;
}
</style>
5. watch监听属性
详情可了解:Vue3:watch 的使用场景及常见问题
5.1 watch()
-
第一个参数是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
-
第二个参数是cb回调函数:(newVal,oldVal,onCleanup)
-
第三个参数是options配置项(一个对象):
deep: true // 是否开启深层监听
immediate: true // 是否立即调用一次
flush: 'pre ’ | ‘sync’ | ‘post’ // 更新时机
onTrack:函数,具备 event 参数,调试用。将在响应式 property 或 ref 作为依赖项被追踪时被调用
onTrigger:函数,具备 event 参数,调试用。将在依赖项变更导致副作用被触发时被调用。
ref监听深层属性需要开启深层监听,深层监听引用类型旧值与新值一样
reactive,隐性开启深层监听
监听属性单一值,需将其变为getter 函数
注意: 深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
import { watch, reactive, ref } from 'vue'
let msg1 = reactive({
one: {
two: {
three: '内容',
},
},
})
let msg2 = ref<string>('测试')
let msg3 = ref<string>('多多')
let msg4 = ref(1)
let msg5 = ref(2)
watch(
()=> msg1.one.two.three,
(newVal, oldVal) => {
console.log('newVal, oldVal :>> ', newVal, oldVal)
}
)
watch(
[msg2, msg3],
(newVal, oldVal) => {
console.log('newVal, oldVal :>> ', newVal, oldVal)
}
)
watch(
[msg3, ()=> msg4.value + msg5.value],
(newVal, oldVal) => {
console.log('newVal, oldVal :>> ', newVal, oldVal)
}
)
onCleanup: onCleanup
接受一个回调函数,这个回调函数,在触发下一次 watch 之前会执行,因此,可以在这里,取消上一次的网络请求,亦或做一些内存清理及数据变更等任何操作。
作用场景: 监听数据变化发起网络请求时
let count = 2;
const loadData = (data) =>
new Promise((resolve) => {
count--;
setTimeout(() => {
resolve(`返回的数据为${data}`);
}, count * 1000);
});
// 此时如果直接监听,两次数据变更时间太短,导致最后页面展示的data数据更新为 ’返回的数据为李四‘
// 原因:数据每次变化,都会发送网络请求,但是时间长短不确定,所以就有可能导致,后发的请求先回来了,所以会被先发的请求返回结果给覆盖掉。
setTimeout(() => {
state.name = '李四';
}, 100);
setTimeout(() => {
state.name = '王五';
}, 200);
// 第二次更新时间在第一次网络请求结束之前
watch(
() => state.name,
(newValue, oldValue, onCleanup) => {
let isCurrent = true;
onCleanup(() => {
// 在下次监听更新之前执行
isCurrent = false;
});
// 模拟网络请求
loadData(newValue).then((res) => {
// 取消上次网络请求,上次网络请求还没完成就将isCurrent设置为false, 则不会变成第一次网络请求的结果,顺序执行第二次监听的结果
if (isCurrent) {
data.value = res;
}
});
}
);
5.2 watchEffect()
watch()
是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
配置项
副作用刷新时机 flush 一般使用post
pre | sync | post | |
---|---|---|---|
更新时机 | 组件更新前执行 | 强制效果始终同步触发 | 组件更新后执行 |
其他配置项:onTrack函数,onTrigger函数
let msg1 = ref('多多测试')
let msg2 = ref('小多')
watchEffect(() => {
console.log('watchEffect监听 : 默认执行顺序等同于开启立即执行的watch');
const dom1 = document.querySelector('#dom1')
console.log('dom1 :>> ', dom1)
console.log('msg1 :>> ', msg1)
})
watchEffect(() => {
console.log('watchEffect监听 : flush: "post"');
const dom1 = document.querySelector('#dom1')
console.log('post组件更新后执行dom1 :>> ', dom1)
}, {
flush: 'post'
})
watch(
msg1,
(newVal, oldVal) => {
console.log('watch监听 : ');
const dom1 = document.querySelector('#dom1')
console.log('dom1 :>> ', dom1)
console.log('newVal,oldVal :>> ', newVal, oldVal)
},
{
immediate: true,
}
)
- watchEffect 默认监听,也就是默认第一次就会执行;
- 不需要设置监听的数据,在 effect 函数中,用到了哪个数据,会自动进行依赖,因此不用担心类似 watch 中出现深层属性监听不到的问题;
- 只能获取到新值,由于没有提前指定监听的是哪个数据,所以不会提供旧值。
watchEffect监听可能出现的问题:
在异步任务(无论是宏任务还是微任务)中进行的响应式操作,watchEffect 无法正确的进行依赖收集。所以后面无论数据如何变更,都不会触发 effect 函数。
解决方法:
如果真的需要用到异步的操作,可以在外面先取值,再放到异步中去使用
清除副作用
watchEffect((onInvalidate) => {
console.log('msg1 :>> ', msg1)
onInvalidate(() => {
// 第一次不执行
console.log('before')
})
})
停止监听
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
5.3 总结
- 当监听 Reactive 数据时:
deep
属性失效,会强制进行深度监听;- 新旧值指向同一个引用,导致内容是一样的。
- 当
watch
的source
是RefImpl
类型时:- 直接监听 state 和 监听 () => state.value 是等效的;
- 如果 ref 定义的是引用类型,并且想要进行深度监听,需要将 deep 设置为 true。
- 当
watch
的source
是函数时,可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化,需要将deep
设置为true
; - 如果想监听多个值的变化,可以将
source
设置为数组,内部可以是Proxy
对象,可以是RefImpl
对象,也可以是具有返回值的函数; - 在监听组件
props
时,建议使用函数的方式进行watch
,并且希望该prop
深层任何属性的变化都能触发,可以将deep
属性设置为true
; - 使用
watchEffect
时,注意在异步任务中使用响应式数据的情况,可能会导致无法正确进行依赖收集。如果确实需要异步操作,可以在异步任务外先获取响应式数据,再将值放到异步任务里进行操作。
6. 组件
6.1 组件的生命周期
beforeCreate
和created
两个生命周期在setup语法糖模式是没有的,用setup去代替onBeforeMount
时读不到dom元素,onMouted
以及之后的生命周期可以读取到dom元素。onBeforeUpdate
获取的是更新之前的dom,onUpdated
获取的是更新之后的domonRenderTracked
和onRenderTriggerd
用于调试,获取收集依赖
6.2 全局组件的注册以及批量注册
全局注册
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent .vue'
...
const app = createApp(App)
// 全局注册
app.component('MyComponent', MyComponent)
...
app.mount('#app')
批量注册:例如elmUI的icon
// main.ts
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
6.3 defineProps(父给子传值)
<!-- 父组件 -->
<A :list="[111, 222, 333]" :msg="msgFather"></A>
<el-divider> 无传递值 </el-divider>
<A></A>
// ts版本
// const props = defineProps<{msg:string}>() /* 不需要定义默认值时 */
// 定义默认值需要使用 withDefaults --- ts专有的
const props = withDefaults(defineProps<{ msg: string, list: number[] }>(), {
msg: '默认值',
list: () => []
})
// js版本
// const props = defineProps({
// msg: {
// type: String,
// default: '默认值'
// },
// list: {
// type: Array,
// default: () => []
// }
// })
// ts
// 也可以将类型声明提取出来,传递数据多时推荐
type Props = { msg: string, list: number[] }
const props = withDefaults(defineProps<Props>(), {
msg: '默认值',
list: () => []
})
// 也可以使用响应性语法糖结构默认值 ---目前为实验性的需要显式启用
const { msg = '默认值', list= []} = defineProps<Props>()
响应性语法糖
响应性语法糖
6.4 defineEmits(子给父传值)
在 <script setup>
中,emit
函数的类型标注也可以通过运行时声明或是类型声明进行:
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
例子:
<!-- 父组件 -->
<A @on-click="getMsg" @change="getMsg2"></A>
<script setup lang="ts">
import { Ref } from 'vue';
import A from '@/components/A.vue'
const getMsg = (data: Ref<string>) => {
console.log('data :>> ', data);
}
const getMsg2 = (id: number) => {
console.log('id :>> ', id);
}
</script>
<!-- 子组件 -->
<template>
<el-button @click="send">给父组件传值</el-button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const sonMsg = ref('子组件内容')
// ts标注版本
// 第一个参数:名字,第二个参数:传递的参数
const emit = defineEmits<{
(e: 'on-click', sonMsg:Ref<string>):void
(e: 'change', id: number):void
}>()
// js版本
// const emit = defineEmits(['on-click', 'change'])
const send = () => {
emit('on-click', sonMsg)
emit('change', 1111)
}
</script>
6.5 defineExpose
可以通过 defineExpose 编译器宏来显式指定在 <script setup>
组件中要暴露出去的属性:
<script setup lang="ts">
import { ref } from 'vue'
const sonMsg = ref('子组件内容')
const name = ref('多多')
const sonFn = () => {
console.log('子组件中的方法 :>> ', name);
}
defineExpose({
name,
age: 18,
sonFn,
fn1: () => console.log('1 :>> ', sonMsg)
})
</script>
<!-- 父组件接收 -->
<template>
<div class="app-container">
<div>子给父传递的内容:</div>
<hr class="mtb20" />
<AVue ref="aRef"></AVue>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import AVue from '@/components/A.vue'
const aRef = ref<InstanceType<typeof AVue> | null>(null)
console.log(aRef.value?.name); // 组件还没挂载,此时为 undefiend
onMounted(()=>{
console.log(aRef.value?.age) // 18
console.log( aRef.value?.fn1()); // 1 :>> ref('子组件内容')
})
</script>
应用场景: 例如elm表单的方法
6.6 递归组件
<!-- Tree组件 -->
<template>
<div @click.stop="clickTree(item, $event)" :style="{ 'marginLeft': '10px' }" v-for="item in treeData">
<input v-model="item.checked" type="checkbox"> <span>{{ item.name }}</span>
<!-- 可以不用定义组件名称直接使用Tree, 但为了防止文件名更改还是自定义名称好 -->
<TreeItem v-if="item?.children?.length" :treeData="item?.children"></TreeItem>
</div>
</template>
<script lang="ts">
// 自定义名称
export default {
name:"TreeItem"
}
</script>
<script setup lang='ts'>
export type TreeType = {
name: string
checked: boolean
children?: TreeType[]
}
defineProps<{
treeData?: TreeType[]
}>()
/* 使用插件对组件命名 */
// defineOptions({
// name: 'TreeItem',
// })
const clickTree = (item: TreeType, e: Event) => {
console.log(item, e);
}
</script>
<!-- 父组件 -->
<template>
<Tree :treeData="data"></Tree>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import Tree, {TreeType} from '@/components/Tree.vue'
const data: TreeType[] = reactive([{
name: '1',
checked: false
}])
</script>
使用unplugin-vue-define-options进行命名
npm i unplugin-vue-define-options -D
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["unplugin-vue-define-options/macros-global" /* ... */]
}
}
// vite
// vite.config.ts
import DefineOptions from 'unplugin-vue-define-options/vite'
import Vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [Vue(), DefineOptions()],
})
6.7 动态组件
<template>
<div class="tabs">
<div @click="switchTab(item)" :class="[currentCom == item.com ? 'active' : '']" class="tab" v-for="item in tabsData"> {{ item.name }}</div>
</div>
<hr class="mtb20">
<component :is="currentCom"></component>
</template>
<script setup lang="ts">
import AVue from '@/components/A.vue';
import BVue from '@/components/B.vue';
import CVue from '@/components/C.vue';
import { reactive, shallowRef, markRaw, AllowedComponentProps, ComponentCustomProps, ComponentOptionsMixin, DefineComponent, ExtractPropTypes, VNodeProps } from 'vue';
type Com = DefineComponent<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, VNodeProps & AllowedComponentProps & ComponentCustomProps, Readonly<ExtractPropTypes<{}>>, {}>
// 注意:不要使用ref,使用shallowRef进行性能优化:绕开深度响应,对深层对象不做处理
const currentCom = shallowRef(AVue)
const tabsData = reactive([{
name: 'A组件',
com: markRaw(AVue) /* 使用markRaw包裹,不进行proxy代理,即不进行响应式处理 */
}, {
name: 'B组件',
com: markRaw(BVue)
}, {
name: 'C组件',
com: markRaw(CVue)
}])
const switchTab = (e: { com: Com; }):void => {
currentCom.value = e.com
}
</script>
<style lang="less" scoped>
@border: #ccc;
.tabs {
display: flex;
align-items: center;
.tab {
border: 1px solid @border;
padding: 15px 15px 20px 15px;
margin: 0 10px;
cursor: pointer;
}
.active {
background-color: #7db6eb;
}
}
</style>
注意: reactive 会进行proxy 代理 而我们组件代理之后毫无用处 节省性能开销 推荐我们使用 shallowRef
或者 markRaw
跳过proxy 代理
markRaw
将一个对象标记为不可被转为代理。返回该对象本身。
类型
function markRaw<T extends object>(value: T): T
示例
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
7. 插槽
- 插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件
v-slot
有对应的简写#
,因此<template v-slot:header>
可以简写为<template #header>
。
7.1 匿名插槽
<!-- 子组件 -->
<button class="fancy-btn">
<!-- 插槽出口 -->
<slot>里面可以填写默认内容</slot>
</button>
<!-- 父组件 -->
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
<!-- 或 -->
<FancyButton>
<template v-slot>
Click me!
</template>
</FancyButton>
<!-- 或 -->
<FancyButton>
<template #default>
Click me!
</template>
</FancyButton>
7.2 具名插槽
<!-- 子组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
<template #default>
<!-- main插槽的内容放这里 -->
</template>
<template #footer>
<!-- footer插槽的内容放这里 -->
</template>
<!-- 隐式的默认插槽 -->
...
</BaseLayout>
注意: 当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 节点都被隐式地视为默认插槽的内容。
7.3 动态插槽
动态指令参数
在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
<script setup lang="ts">#default
import BaseLayout from '@/components/base-layout.vue';
import { ref} from 'vue';
const dynamicSlotName = ref<string>('header')
</script>
7.4 作用域插槽
使用场景: 父组件可以拿到子组件的值,在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据
v-slot="slotProps"
可以类比这里的函数签名,和函数的参数类似,我们也可以在v-slot
中使用解构
<AVue>
<template #header="headerProps">
标题: {{ headerProps.headerMsg }}
</template>
<template #default="{ index, data }">
<div>{{ index }} --- {{ data.name }} --- {{ data.age }}</div>
</template>
<template #footer="{ footerMsg }">
底部: {{ footerMsg }}
</template>
</AVue>
<template>
<slot name="header" :headerMsg="msg1"></slot>
<hr class="mtb20">
<div v-for="(item, index) in dataSlot">
<slot :index="index" :data="item"></slot>
</div>
<hr class="mtb20">
<slot name="footer" :footerMsg="msg2"></slot>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
type SlotType = {
name: string,
age: number
}
const msg1 = ref<string>('信息1')
const msg2 = ref<string>('信息2')
const dataSlot = reactive<SlotType[]>([
{
name: '多多',
age: 20
}, {
name: '小多',
age: 18
}, {
name: '图图',
age: 19
}
])
</script>
8. 内置组件
8.1 异步组件&代码分包&suspense
顶层 await
<script setup>
中可以使用顶层 await
。结果代码会被编译成 async setup()
:
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
注意: async setup()
必须与 Suspense
内置组件组合使用,Suspense
目前还是处于实验阶段的特性,会在将来的版本中稳定。
异步组件以及defineAsyncComponent()方法
defineAsyncComponent
defineAsyncComponent()
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
类型:
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise<Component>
interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
异步组件
异步组件详情
通过 defineAsyncComponent
加载异步配合import 函数模式
便可以进行代码分包(代码分割详情)
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
// 完整写法
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
Suspense
Suspense详情
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
<Suspense>
可以等待的异步依赖有两种:
- 带有异步
setup()
钩子的组件。这也包含了使用<script setup>
时有顶层await
表达式的组件。 - 异步组件。
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
案例可见:vue3笔记案例——Suspense使用之骨架屏
vue3笔记案例——Suspense使用之骨架屏
8.2 Transition&TransitionGroup动画组件
详情见:vue3学习笔记之Transition&TransitionGroup
8.3 Teleport传送组件
运用场景: 一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。例如 全屏的模态框
主要解决问题: 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响
基本使用
<Teleport>
接收一个 to
prop 来指定传送的目标。to
的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。
<!-- 标签名 -->
<Teleport to="body">
<div>xxx</div>
</Teleport>
<!-- 类名 -->
<Teleport to=".xxx">
<div>xxx</div>
</Teleport>
<!-- id名 -->
<Teleport to="#xxx">
<div>xxx</div>
</Teleport>
案例可见:vue3笔记案例——Teleport使用之模态框
vue3笔记案例——Teleport使用之模态框
注意: <Teleport>
挂载时,传送的 to
目标必须已经存在于 DOM
中。理想情况下,这应该是整个 Vue
应用 DOM
树外部的一个元素。如果目标元素也是由 Vue
渲染的,你需要确保在挂载 <Teleport>
之前先挂载该元素。
禁用 Teleport
:disabled
设置为 true
时,teleport
不生效,可以传递一个 props
动态控制 teleport
<teleport :disabled="true" to='body'>
<A></A>
</teleport>
8.4 KeepAlive缓存组件
<KeepAlive>
是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例
<!-- 非活跃的组件将会被缓存! -->
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>
<!-- 多个条件判断的子组件 -->
<keep-alive>
<ComA v-if="a > 1"></ComA>
<ComB v-else></ComB>
</keep-alive>
<!-- 和 `<transition>` 一起使用 -->
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
注意: 在 DOM 模板中使用时,它应该被写为 <keep-alive>
。
包含/排除(include/exclude)
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 include
和 exclude
prop 来定制该行为。这两个 prop
的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
<!-- 它会根据组件的 name 选项进行匹配,所以组件如果想要条件性地被 KeepAlive 缓存,就必须显式声明一个 name 选项。 -->
<!-- 只缓存A,B组件 -->
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="A,B">
<component :is="view" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/A|B/">
<component :is="view" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :include="['A', 'B']">
<component :is="view" />
</KeepAlive>
<!-- 不缓存C组件 -->
<KeepAlive :exclude="['C']">
<component :is="view" />
</KeepAlive>
最大缓存实例数(max)
我们可以通过传入 max
prop 来限制可被缓存的最大组件实例数。<KeepAlive>
的行为在指定了 max
后类似一个 LRU 缓存
:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
<KeepAlive :max="10">
<component :is="activeComponent" />
</KeepAlive>
缓存实例的生命周期
当一个组件实例从 DOM 上移除但因为被 <KeepAlive>
缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。
<script setup>
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
// 缓存组件中
// 只执行一次,可以在此生命周期中执行只需初始化一次的内容
onMounted(()=>{
console.log('初始化')
})
// 类似普通组件中的onMounted,每次激活都会执行
onActivated(()=>{
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
console.log('onActivated————初始化')
})
// 类似普通组件中的onUnmounted,但不会卸载组件,而是将组件变为不活跃状态并缓存
onDeactivated(()=>{
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
console.log('onDeactivated————组件失活')
})
// 不执行
onUnmounted(()=>{
console.log('组件卸载')
})
</script>
注意:
onActivated
在组件挂载时也会调用,并且onDeactivated
在组件卸载时也会调用。- 这两个钩子不仅适用于
<KeepAlive>
缓存的根组件,也适用于缓存树中的后代组件。
案例:
示例图
代码
<template>
<label><input type="radio" v-model="current" :value="A" /> A</label>
<label class="mlr10"><input type="radio" v-model="current" :value="B" /> B</label>
<label><input type="radio" v-model="current" :value="C" /> C</label>
<hr class="mtb20"/>
<!-- 不对C组件进行缓存 -->
<KeepAlive :exclude="['C']">
<component :is="current"></component>
</KeepAlive>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
import A from '@/components/A.vue'
import B from '@/components/B.vue'
import C from '@/components/C.vue'
const current = shallowRef(A)
</script>
<!-- A组件 -->
<template>
<p>组件: A</p>
<div class="mtb20">
<span :style="{ 'marginRight': '1rem' }">count: {{ count }}</span>
<el-button @click="count++"> + </el-button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
</script>
<!-- B组件 -->
<template>
<p>组件: B</p>
<div class="mtb20">
<span :style="{ 'marginRight': '1rem' }">信息: {{ msg }}</span>
<input v-model="msg" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const msg = ref<string>('')
</script>
<!-- C组件 -->
<template>
<p>组件: C</p>
<div class="mtb20">
<span :style="{ 'marginRight': '1rem' }">不缓存信息: {{ msg }}</span>
<input v-model="msg" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const msg = ref<string>('')
</script>
9. 依赖注入(Provide/Inject)
一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
9.1 Provide(提供)
<script setup>
import { provide } from 'vue'
// 第一个参数是注入名(字符串或 Symbol),第二个参数是值(任意类型)
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
9.2 Inject (注入)
<script setup lang="ts">
import { inject } from 'vue'
const message = inject<string>('message')
</script>
注入默认值
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject<string>('message', '这是默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass())
如果默认值是函数不想被当作工厂函数则需要传递第三个参数 false
const value = inject('key', () => {}, false)
和响应式数据配合使用
建议尽可能将任何对响应式状态的变更都保持在供给方组件中。
<!-- 在供给方组件内 -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const location = ref<string>('North Pole')
function updateLocation():void {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
<script setup lang="ts">
import { inject, Ref } from 'vue'
type Location = {
location: Ref<string>
updateLocation: () => void
}
const { location, updateLocation } = inject('location') as Location
</script>
不想要后代组件更改祖先组件提供的数据,使用 readonly()
包裹使其变为只读
<script setup lang="ts">
import { provide, ref, readonly } from 'vue';
const num = ref<number>(1)
provide('num', readonly(num))
</script>
子孙组件使用提供的数据时,类型为 unknow
,解决方法:
- 使用
as
断言:const num = inject('num') as Ref<number>
- 使用
!
:num!.value
- 注入时使用默认值:
const num = inject('num', ref(1))
案例
<!-- 祖先组件 -->
<template>
<div class="f24 fw700 mtb20">祖先组件</div>
<el-button @click="num++">数字+</el-button>
<el-button @click="num--">数字-</el-button>
<span class="ml1">{{ num }}</span>
<div class="mtb20">
<label class="mr1"><input v-model="color" value="red" type="radio"> 红色</label>
<label class="mr1"><input v-model="color" value="yellow" type="radio"> 黄色</label>
<label class="mr1"><input v-model="color" value="blue" type="radio"> 蓝色</label>
</div>
<div class="box"></div>
<hr class="mtb20">
<A></A>
</template>
<script setup lang="ts">
import { provide, ref, readonly } from 'vue';
import A from '@/components/A.vue';
const color = ref<string>('red')
const num = ref<number>(1)
provide('color', color)
provide('num', readonly(num))
</script>
<style lang="less" scoped>
.box {
width: 100px;
height: 100px;
background-color: v-bind(color); // v-bind可以直接绑定setup中的变量
}
</style>
<!-- 父亲组件 -->
<template>
<div class="f24 fw700 mtb20">父亲组件</div>
<div class="box"></div>
<hr class="mtb20">
<B></B>
</template>
<script setup lang="ts">
import { inject, Ref } from 'vue';
import B from '@/components/B.vue';
const color = inject<Ref<string>>('color')
</script>
<style lang="less" scoped>
.box {
width: 100px;
height: 100px;
background-color: v-bind(color);
}
</style>
<!-- 子孙组件 -->
<template>
<div class="f24 fw700 mtb20">子孙组件</div>
<el-button @click="changeColor">改变颜色</el-button>
<el-button @click="changeNum">改变数字</el-button>
<div class="box mtb20"></div>
<div>数字:{{ num }}</div>
</template>
<script setup lang="ts">
import { inject, Ref } from 'vue';
const color = inject<Ref<string>>('color')
const num = inject('num') as Ref<number>
// 不推荐在后代组件中直接修改祖先组件提供的数据,应该在祖先组件写修改方法并一起provide
const changeColor = () => {
color!.value = 'green'
}
const changeNum = () => {
num.value++
}
</script>
<style lang="less" scoped>
.box {
width: 100px;
height: 100px;
background-color: v-bind(color);
}
</style>
9.3 使用 Symbol 作注入名
如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol
来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol
:
// keys.js
import { InjectionKey } from 'vue'
export const myInjectionKey = Symbol() as InjectionKey<string>
// 在供给方组件中
import { provide, InjectionKey } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey , 'foo') // 若提供的是非字符串值会导致错误
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const foo = inject<string>(myInjectionKey) // 类型:string | undefined,此时数据类型为unknow
// 方法一:定义一个默认值
const foo = inject<string>('foo', 'bar') // 类型:string
// 方法二:as类型断言
const foo = inject('foo') as string
10. 兄弟组件传参以及Mitt
兄弟组件传参的几种方式:
- 借助父组件传参:父组件充当桥梁,缺点是处理复杂繁琐,逻辑结构不清晰
- 发布订阅模式:event-bus,缺点vue3取消了
10.1 event-bus
简易的一个bus.ts
// bus.ts
type BusClass<T> = {
emit: (name: T) => void
on: (name: T, callback: Function) => void
}
// 定义参数类型
type BusParams = string | number | symbol
type List = {
[key: BusParams]: Array<Function>
}
class Bus<T extends BusParams> implements BusClass<T> {
list: List
constructor() {
this.list = {}
}
emit(name: T, ...args: Array<any>){
let eventName: Array<Function> = this.list[name]
eventName.forEach(e => {
e.apply(this, args)
});
}
on(name: T, callback: Function){
let fn: Array<Function> = this.list[name] || []
fn.push(callback)
this.list[name] = fn
}
}
export default new Bus()
<!-- A组件 -->
<template>
<el-button @click="emitB">派发</el-button>
</template>
<script setup lang="ts">
import Bus from '@/utils/Bus'
let flag = true
const emitB = () => {
flag = !flag
Bus.emit('emit-flag', flag)
}
</script>
<style lang="less" scoped>
</style>
<!-- B组件 -->
<template>
<div>接收:{{ flag }}</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import Bus from '@/utils/Bus'
const flag = ref<boolean>(true)
Bus.on('emit-flag', (e: boolean) => {
flag.value = e
})
</script>
<style lang="less" scoped>
</style>
10.2 Mitt
github地址:Mitt
安装
npm install mitt -S
main.ts挂载全局属性
import { createApp } from 'vue'
import mitt from 'mitt'
...
const app = createApp({})
const Mitt = mitt()
// TS
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
$bus: typeof Mitt
}
}
app.config.globalProperties.$bus = Mitt
...
app.mount('#app')
使用
派发(emit)
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance()
let flag = true
const emitB = () => {
flag = !flag
instance?.proxy?.$bus.emit('on-flag', flag)
}
接收(on)
import { getCurrentInstance, ref } from 'vue';
const instance = getCurrentInstance()
const flag = ref<boolean>(true)
instance?.proxy?.$bus.on('on-flag', (e) => {
flag.value = e as boolean
})
// *:监听所有事件,回调函数:参数一(事件名) 参数二(传递值)
instance?.proxy?.$bus.on('*', (type, e) => {
console.log('type,e :>> ', type, e);
})
删除(off)
...
const Bus = (e: any) => {
flag.value = e as boolean
}
instance?.proxy?.$bus.off('on-flag', Bus)
// 删除全部事件
instance?.proxy?.$bus.all.clear()
11. TSX
vue3中使用tsx详情可见:vue3学习笔记之TSX的使用
12. v-model
<input v-model="text">
<!-- v-model实际上是以下的简写版本 -->
<input
:value="text"
@input="event => text = event.target.value">
12.1 组件中的v-model
<CustomInput v-model="searchText" />
<!-- 等价于 -->
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
组件中 v-model
的实现是通过 defineProps
和 defineEmits
结合实现的
案例:v-model的实现
<template>
<!-- 父组件 -->
<el-button @click="isShow = !isShow">显示/隐藏</el-button> {{ isShow }}
<div>msg: {{ msg }}</div>
<hr class="mtb20">
<A v-model="isShow" v-model:textVal="msg"></A>
</template>
<script setup lang="ts">
import A from '@/components/A.vue';
const isShow = ref(true)
const msg = ref('多多')
</script>
<!-- A组件 -->
<template>
<div v-if="modelValue">
<el-button @click="close">隐藏</el-button>
<input @input="changeInput" :value="textVal" type="text">
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: boolean
textVal: string
}
defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', flag: boolean): void
(e: 'update:textVal', msg: string): void
}>()
const changeInput = (e: Event) => {
const val = (e.target as HTMLInputElement).value
emit('update:textVal', val)
}
const close = () => {
emit('update:modelValue', false)
}
</script>
12.2 内置修饰符
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
<!-- 等价于 -->
<input
:value="msg"
@change="event => msg = event.target.value">
.number
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
<input v-model.number="age" />
<!-- 类似于 -->
<input
:value="msg"
@input="event => msg = parseFloat(event.target.value)">
注意: number
修饰符会在输入框有 type="number"
时自动启用。
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
<input v-model.trim="msg" />
<!-- 等价于 -->
<input
:value="msg"
@input="event => msg = event.target.value.trim()">
注意: number
修饰符会在输入框有 type="number"
时自动启用。
12.3 自定义修饰符Modifiers
基本使用
xxxModifiers
//定义
interface Props {
modelValue: boolean
textVal: string
modelModifiers?: {
noClose: boolean
}
textValModifiers?: {
isDD: boolean
capitalize: boolean
}
}
const props = defineProps<Props>()
// 使用
const noClose = props?.modelModifiers?.noClose
例子
interface Props {
modelValue: boolean
textVal: string
modelModifiers?: {
noClose: boolean
}
textValModifiers?: {
isDD: boolean
capitalize: boolean
}
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', flag: boolean): void
(e: 'update:textVal', msg: string): void
}>()
const changeInput = (e: Event) => {
let val = (e.target as HTMLInputElement).value
const isDD = props?.textValModifiers?.isDD
const capitalize = props?.textValModifiers?.capitalize
if(isDD) {
val = `多多的${val}`
}
if(capitalize) {
val = val.charAt(0).toUpperCase() + val.slice(1)
}
emit('update:textVal', val)
}
const close = () => {
const val = props?.modelModifiers?.noClose ? true : false
emit('update:modelValue', val)
}
<A v-model.noClose="isShow" v-model:textVal.capitalize="msg"></A>
13. 全局API
官方文档
13.1 app.config.globalProperties
用法
const app = createApp()
// TS
// 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
msg: string
}
}
app.config.globalProperties.msg = 'hello'
<!-- template中,直接使用 -->
<template>
<div>
{{msg}}
</div>
</template>
//js中
// 使用一
const instance = getCurrentInstance()
const msg = instance?.proxy?.msg
// 使用二
const { appContext } = <ComponentInternalInstance>getCurrentInstance()
const msg = appContext.config.globalProperties.msg
console.log(msg);
13.2 nextTick()
等待下一次 DOM 更新刷新的工具方法。
<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
EventLoop
从一道题浅说 JavaScript 的事件循环
事件循环中先执行宏任务再清空里面所有微任务,然后执行新的宏任务
异步任务分为宏任务和微任务
常见的宏任务:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、setImmediate(Node.js 环境)、ajax、callback
常见的微任务:promise.then()中、MutaionObserver、process.nextTick(node环境)
注意:promise
本身是同步的,但它的方法是异步的,且属于微任务
例
console.log(1) // 直接执行
setTimeout(() => {
console.log(2) // 进入宏任务队列
})
new Promise((resolve) => {
console.log(3) //直接执行
resolve()
})
.then(() => {
console.log(4) // 进入微任务队列
})
.then(() => {
console.log(5)
})
console.log(6) // 直接执行
// 执行顺序:1 3 6 4 5 2
上述,执行顺序看似先执行的微任务,实际上是由于script(整体代码)
本身是一个宏任务,所以在执行同步任务后先清空微任务,在执行新的宏任务
console.log(1) // 直接执行
// 进入宏任务队列
setTimeout(() => {
console.log(2)
}, 10)
// 进入宏任务队列
setTimeout(() => {
new Promise((resolve) => {
console.log(3) //直接执行
resolve()
}).then(() => {
console.log(4) // 进入微任务队列
})
})
new Promise((resolve) => {
console.log(5) //直接执行
// 进入微任务队列
resolve()
console.log(6) //直接执行
// 进入宏任务队列
setTimeout(() => console.log(7))
})
.then(() => {
setTimeout(() => console.log(8))
})
.then(() => {
console.log(9)
})
console.log(10) // 直接执行
// 1 5 6 10 9 3 4 7 8 2
一次执行一个宏任务,清空完宏任务里的微任务后执行下一个,即先跳过其他宏任务以及里面的微任务
14. 自定义指令
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
在没有使用 <script setup>
的情况下,自定义指令需要通过 directives
选项注册:
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
14.1 指令钩子
一般常用 mounted
、updated
、unmounted
三个钩子函数
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
钩子参数
指令的钩子会传递以下几种参数:
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层 VNode。prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
<template>
<div v-color:height.bg="{ width: '240px', height: '200px', color: 'red' }"></div>
</template>
<script setup lang="ts">
import { Directive } from 'vue';
const vColor: Directive = {
mounted(el, binding) {
el.style.width = binding.value?.width
if (binding.modifiers?.bg) {
el.style.backgroundColor = binding.value?.color
} else {
el.style.color = binding.value?.color
}
if (binding.arg === 'height') {
el.style.height = binding.value?.width
} else {
el.style.height = binding.value?.height
}
}
}
</script>
14.2 简写形式
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted
和 updated
上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
<div v-color="color"></div>
// 全局
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
// 组件
const vColor: Directive = (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
}
案例:简单实现权限指令dome
<template>
<!-- 父组件 -->
<el-button v-has-show="'index:add'">新 增</el-button>
<el-button v-has-show="'index:edit'">修 改</el-button>
<el-button v-has-show="'index:delete'">删 除</el-button>
</template>
<script setup lang="ts">
import { Directive } from 'vue';
// mock登陆返回的用户信息
localStorage.setItem('userId', 'dd')
// mock后台返回的权限数组
// *:*:* ===> 用户id:当前信息页:权限
const permission = [
'dd:index:add',
'dd:index:edit',
'dd:index:delete',
'xd:index:add',
'xd:index:edit'
]
const userId = localStorage.getItem('userId')
const vHasShow: Directive = (el, binding) => {
if (!permission.includes(`${userId}:${binding.value}`)) {
// 隐藏
el.parentNode && el.parentNode.removeChild(el)
}
}
</script>
案例:自定义指令实现拖拽效果
<div v-move class="box"></div>
move.ts
import type { Directive } from 'vue'
const vMove: Directive = (el) => {
let moveEl = (el?.firstElementChild as HTMLElement) || el;
const mouseDown = (e: MouseEvent) => {
el.style.position = 'absolute'
el.style.zIndex = 999
// 点击位置相对于盒子的偏移量 = 鼠标位置 - 盒子偏移量
const x = e.clientX - el.offsetLeft
const y = e.clientY - el.offsetTop
/*
边界值设定
左/上边界 = x/y鼠标偏移量
下/右边界 = 浏览器宽高 - 盒子宽高
*/
const bottomLimit = window.innerHeight - el.clientHeight
const rightLimit = window.innerWidth - el.clientWidth
const move = (e) => {
// 移动位置(以左上角为基准) = 新鼠标位置 - 鼠标初始盒子偏移量
const moveY = e.clientY - y < 0 ? y : (e.clientY - y > bottomLimit ? bottomLimit : e.clientY - y + 'px')
const moveX = e.clientX - x < 0 ? x : (e.clientX - x > rightLimit ? rightLimit : e.clientX - x + 'px')
el.style.top = moveY
el.style.left = moveX
}
// 监听鼠标移动事件
document.addEventListener('mousemove', move)
// 鼠标抬起取消鼠标移动事件的监听
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', move)
})
}
moveEl.addEventListener('mousedown', mouseDown)
}
export default vMove
main.ts
全局注册指令
import { createApp } from 'vue'
import vMove from '@/directive/move'
...
const app = createApp({})
app.directive('move', vMove)
app.mount('#app')
15. 组合式函数——“vue的hooks”
官网地址:组合式函数
vueUse库
15.1 基本使用
命名
组合式函数约定用驼峰命名法命名,并以 “use”
作为开头。
输入参数
尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref()
工具函数会对此非常有帮助:
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
返回值
推荐的约定是组合式函数始终返回一个包含多个 ref
的普通的非响应式对象而不是一个 reactive
对象,这样该对象在组件中被解构为 ref
之后仍可以保持响应性:
// x 和 y 是两个 ref
const { x, y } = useMouse()
mouse.ts
import useEventListener from "./eventListener"
// 按照惯例,组合式函数名以“use”开头
export default function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
useEventListener(window, 'mousemove', update)
// 通过返回值暴露所管理的状态
return { x, y }
}
eventListener.ts
// 按照惯例,组合式函数名以“use”开头
export default function useEventListener (target: any, event: string, callback: Function) {
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
组件
<template>
<div>x: {{ x }}----y: {{ y }}</div>
</template>
<script setup lang="ts">
import useBase64 from '@/hooks/useBase64'
const { x, y } = useMouse()
</script>
16. 插件
一个插件可以是一个拥有 install()
方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use()
的额外选项作为参数:
const myPlugin = {
install(app, options) {
// 配置此应用
}
}
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})
插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:
- 通过
app.component()
和app.directive()
注册一到多个全局组件或自定义指令。 - 通过
app.provide()
使一个资源可被注入进整个应用。 - 向
app.config.globalProperties
中添加一些全局实例属性或方法 - 一个可能上述三种都包含了的功能库 (例如
vue-router
)。
17. 样式穿透及CSS 新特性
详情见:vue3学习笔记之样式穿透(:deep)及CSS 新特性(:soltted、:gloabl、v-bind、mouldCSS)
18. h函数
创建虚拟 DOM 节点 (vnode)。
类型
// 完整参数签名
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
// 省略 props
function h(type: string | Component, children?: Children | Slot): VNode
type Children = string | number | boolean | VNode | null | Children[]
type Slot = () => Children
type Slots = { [name: string]: Slot }
第一个参数既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。第二个参数是要传递的 prop,第三个参数是子节点。
当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。
为了方便阅读,当子节点不是插槽对象时,可以省略 prop 参数。
import { h } from 'vue'
// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })
// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
案例
<template>
<ComA text="这是props传递的内容" @on-click="getBtn">
<template #default>
插槽内容
</template>
</ComA>
</template>
<script setup lang="ts">
import { h } from 'vue'
type Props = {
[key: string]: any
}
const ComA = (props: Props, ctx: any) => {
return h(
'div',
{
class: 'text-center bg-green500 text-white w-20 h-10 lh-10',
onClick: () => {
ctx.emit('on-click', '我点击了')
}
},
// props.text // props传递内容
ctx.slots.default() // 插槽传递内容
)
}
const getBtn = (str: string) => {
console.log(str);
}
</script>
19. 环境变量及proxy代理
19.1 环境变量
环境变量详情请看:vite+vue3环境变量的配置
19.2 proxy代理
vite.config.ts
export default defineConfig({
plugins: [vue()],
server:{
proxy:{
'/api':{
target:"http://localhost:3001/", //跨域地址
changeOrigin:true, //支持跨域
rewrite:(path) => path.replace(/^\/api/, "")//重写路径,替换/api
}
}
}
})
fetch('/api/user')