VUE3 语法
setup
vue3 中用 setup 函数整合了所有的 api;只执行一次,在生命周期函数前执行,所以在 setup 函数中拿不到当前实例 this,不能用 this 来调用 vue2 写法中定义的方法
它将接受两个参数:props、context
1 2 3 4 5 6
| setup(props, context) { return { } }
|
props
setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新
但是,因为 props 是响应式的,不能使用 ES6 解构,因为它会消除 prop 的响应性
如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来安全地完成此操作
1 2 3 4 5 6
| import { toRefs } from 'vue'
setup(props) { const { title } = toRefs(props) console.log(title.value) }
|
context
context 暴露三个组件的 property:{ attrs, slots, emit }
它是一个普通的 JavaScript 对象,不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构
setup方法和以下是等效的
1 2 3
| <script setup>
</script>
|
ref、reactive
ref 可以将某个普通值包装成响应式数据,仅限于简单值,内部是将值包装成对象,再通过 defineProperty 来处理的
通过 ref 包装的值,取值和设置值的时候,需用通过 .value来进行设置
可以用 ref 来获取组件的引用,替代 this.$refs 的写法
reactive 对复杂数据进行响应式处理,它的返回值是一个 proxy 对象,在 setup 函数中返回时,可以用 toRefs 对 proxy 对象进行结构,方便在 template 中使用
ref
使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <template> <div> <div> <ul v-for="ele in eleList" :key="ele.id"> <li>{{ ele.name }}</li> </ul> <button @click="addEle">添加</button> </div> <div> <ul v-for="ele in todoList" :key="ele.id"> <li>{{ ele.name }}</li> </ul> <button @click="addTodo">添加</button> </div> </div> </template>
<script> import { ref, reactive, toRefs } from 'vue'
export default { setup() { const eleList = ref([]) function addEle() { let len = eleList.value.length eleList.value.push({ id: len, name: 'ref 自增' + len }) }
const dataObj = reactive({ todoList: [] }) function addTodo() { let len = dataObj.todoList.length dataObj.todoList.push({ id: len, name: 'reactive 自增' + len }) }
return { eleList, addEle, addTodo, ...toRefs(dataObj) } } } </script>
|
类型
1 2 3 4
| import { ref } from 'vue' import type { Ref } from 'vue'
export const pcImgList: Ref<Array<ImgUrlModel>> = ref([])
|
在 Vue 3 中,使用 ref 创建的对象默认是深层响应式的(因为内部会自动调用 reactive),所以它的属性可以实现双向绑定。
reactive
注意
reactive 不能用于原始类型(如 string, number, boolean)
reactive 是用来创建响应式对象(object)的。
它的参数必须是一个 对象(Object),不能是原始类型(primitive type)。
对比
不要用 reactive 的情况(必须用 ref)
| 场景 |
原因 |
存储原始值(number, string, boolean) |
reactive(0) 无效,会警告或忽略 |
| 需要整体替换对象/数组 |
reactiveObj = newObj 会丢失响应性 |
| 从组合函数返回 |
解构会丢失响应性,ref 更安全 |
在模板中频繁与 v-model 绑定简单值 |
ref 更直接 |
适合使用 reactive 而不用 ref 的典型场景
管理一个结构清晰、稳定的对象状态
当你有一个明确的对象结构,且不会整体替换这个对象时,reactive 写起来更自然,无需 .value。
1 2 3 4 5 6 7 8 9 10
| const formState = reactive({ username: '', email: '', password: '', errors: {} })
formState.username = 'alice'
|
computed
1 2 3 4 5
| let sum = computed( () => dataObj.todoList.length + eleList.value.length ) console.log('setup引用computed要.value:' + sum.value)
|
注意
computed返回的是一个ref对象。
watch
基本
1 2 3 4 5 6
| import { ref, watch } from 'vue'
watch(eleList,(curVal, oldVal) => { console.log('监听器:', curVal, oldVal) })
|
深度监听
1 2 3 4 5 6
| import { ref, watch } from 'vue'
watch(eleList,(curVal, oldVal) => { console.log('监听器:', curVal, oldVal) },{deep: true})
|
路由监听
1 2 3 4 5 6 7 8
| import { ref, reactive, watch } from 'vue' import { useRoute } from 'vue-router'
const route = useRoute()
watch(route, (curVal, oldVal) => { console.log('监听器:', curVal.path) })
|
立即执行
我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:
1 2 3 4 5 6 7
| watch( source, (newValue, oldValue) => { }, { immediate: true } )
|
同时监听多个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { watch } from 'vue';
export default { data() { return { value1: '', value2: '' }; }, created() { watch( () => [this.value1, this.value2], (newValues, oldValues) => { console.log('值变化:', newValues); } ); } };
|
watchEffect
响应式地跟踪函数中引用的响应式数据,当响应式数据改变时,会重新执行函数
1 2 3 4 5 6
| const count = ref(0)
const stop = watchEffect(() => console.log(count.value))
stop()
|
还可以停止监听,watchEffect 返回一个函数,执行后可以停止监听
与 vue2 一样:
1 2 3 4
| const unwatch = this.$watch('say', curVal => {})
unwatch()
|
模板的ref
模板页面中获取
1 2 3 4 5 6 7 8
| <template> <div class="comp_report_table_root" ref="tableOuterRef" > </div> </template>
|
在setup语法糖中
1 2 3 4 5
| const tableOuterRef = ref(null)
onMounted(() => { console.log(tableOuterRef.value) })
|
可以看出我们只要定义同名的ref就可以。
注意
只能在setup中定义才可以。
只能在onMounted中才能获取到。
TS封装
如果TS进行了封装,页面需要传入ref,定义ref必须在setup中。
1 2 3 4 5 6 7 8 9 10 11 12
| <script lang="ts" setup> import { type PropType, ref } from 'vue'
const props = defineProps({ compData: { type: Object as PropType<ReportItem>, required: true } }) import { usePage } from './CompReportTable'
const tableOuterRef = ref()
const {} = usePage(props, tableOuterRef) </script>
|
引用的TS
1 2 3 4 5 6 7 8
| import { ref, onMounted, type Ref } from 'vue'
export function usePage(props: { compData: ReportItem }, tableOuterRef: Ref<HTMLElement>) { onMounted(() => { console.log(tableOuterRef.value) }) return {} }
|
Props
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props。
setup
在使用 <script setup> 的单文件组件中,props 可以使用 defineProps() 宏来声明:
vue
1 2 3 4 5
| <script setup> const props = defineProps(['foo'])
console.log(props.foo) </script>
|
TS
1 2 3 4 5 6 7 8 9
| <script setup lang="ts"> const props = defineProps({ name: { type: String, required: true }, bar: Number })
props.name props.bar </script>
|
注意
props 是 Ref 类型,内部的属性不是。
使用自定义类型
1 2 3 4 5 6 7 8
| interface TGoods { goodsId: number name: string }
const props = defineProps({ goodsDetail: { type: Object as () => TGoods, required: true } })
|
默认值
1 2 3
| const props = defineProps({ maxNum: { type: Number, default: 3 } })
|
非setup
在没有使用 <script setup> 的组件中,props 可以使用 props 选项来声明:
JS
1 2 3 4 5 6 7
| export default { props: ['foo'], setup(props) { console.log(props.foo) } }
|
注意传递给 defineProps() 的参数和提供给 props 选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。
props属性监听
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup> import { defineProps, watch } from 'vue';
const props = defineProps({ sortId: { type: Number, required: true } });
watch(() => props.sortId, (newValue, oldValue) => { console.log(`sortId changed from ${oldValue} to ${newValue}`); }); </script>
|
v-model
v-model 可以在组件上使用以实现双向绑定。
组合式
新版本-单个属性
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:
自定义组件中
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> const model = defineModel()
function update() { model.value++ } </script>
<template> <div>Parent bound v-model is: {{ model }}</div> <button @click="update">Increment</button> </template>
|
父组件可以用 v-model 绑定一个值:
1 2
| <Child v-model="countModel" />
|
defineModel() 返回的值是一个 ref。
它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:
- 它的
.value 和父组件的 v-model 的值同步;
- 当它被子组件变更了,会触发父组件绑定的值一起更新。
defineModel 是一个便利宏。
编译器将其展开为以下内容:
- 一个名为
modelValue 的 prop,本地 ref 的值与其同步;
- 一个名为
update:modelValue 的事件,当本地 ref 的值发生变更时触发。
TS中设置类型
1
| const model = defineModel<number[]>()
|
新版本-多个属性
1 2 3 4 5 6 7 8 9
| <script setup> const firstName = defineModel('firstName') const lastName = defineModel('lastName') </script>
<template> <input type="text" v-model="firstName" /> <input type="text" v-model="lastName" /> </template>
|
父组件可以用 v-model 绑定多个值:
1
| <Child v-model:firstName="firstName" v-model:lastName="lastName" />
|
旧版本-单个属性
在 3.4 版本之前,你一般会按照如下的方式来实现上述相同的子组件:
vue
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup> const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) </script>
<template> <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /> </template>
|
父组件可以用 v-model 绑定一个值:
1 2
| <Child v-model="foo" />
|
内部实际将被编译为:
1 2 3 4 5
| <Child :modelValue="foo" @update:modelValue="$event => (foo = $event)" />
|
旧版本-多个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script setup> defineProps({ firstName: String, lastName: String })
defineEmits(['update:firstName', 'update:lastName']) </script>
<template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
|
选项式
选项式没有新旧版本的区别
单个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script> export default { props: ['modelValue'], emits: ['update:modelValue'] } </script>
<template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>
|
或者
1 2 3 4 5 6 7 8
| export default { props: { modelValue: { type: Number } }, emits: ['update:modelValue'] }
|
可能多个类型
1 2 3 4 5 6 7 8 9 10 11
| export default { props: { modelValue: { type: [Number, String], default() { return ""; } } }, emits: ['update:modelValue'] }
|
使用
1
| <CustomInput v-model="searchText" />
|
多个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <script> export default { props: { firstName: String, lastName: String }, emits: ['update:firstName', 'update:lastName'] } </script>
<template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
|
使用
1 2 3 4
| <UserName v-model:first-name="first" v-model:last-name="last" />
|
事件
1 2 3 4 5 6 7
| <script setup> const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() { emit('submit') } </script>
|
TS中使用
1 2 3 4 5 6
| <script setup lang="ts"> const emit = defineEmits<{ (e: 'pageSubmit', value: number): void (e: 'drawerClose'): void }>() </script>
|
生命周期
通过在生命周期钩子前面加上on来访问组件的生命周期钩子
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们
换句话说,
想在beforeCreate 和 created中编写的任何代码都应该直接在 setup 函数中编写
周期函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| onMounted(() => { console.log('组件挂载') })
onUnmounted(() => { console.log('组件卸载') })
onUpdated(() => { console.log('组件更新') })
onBeforeUpdate(() => { console.log('组件将要更新') })
onActivated(() => { console.log('keepAlive 组件 激活') })
onDeactivated(() => { console.log('keepAlive 组件 非激活') })
|
Vue Router
引用
useRoute、useRouter
1 2 3 4 5 6 7 8
| import {useRoute, useRouter} from 'vue-router'
const route = useRoute() const router = useRouter()
|
注意
useRouter() 必须在 “setup 上下文” 或 “组件生命周期内” 调用
useRouter() 是 Vue Router 提供的组合式 API,其内部依赖 Vue 的 “依赖注入系统” 获取路由实例。它必须满足两个条件:
- 只能在
<script setup> 中直接调用,或在组件的 setup() 函数中调用。
- 不能在非组件环境(如单独的工具函数文件、普通 JS 模块)中调用,也不能在异步回调、定时器等脱离组件上下文的地方调用。
分离的TS中可以这样写
1 2 3 4 5
| import type { Router } from 'vue-router'
export function usePage(router: Router) {
}
|
页面中
1 2 3 4 5 6 7 8
| <script lang="ts" setup> import { useRouter } from 'vue-router'
const router = useRouter() import { usePage } from './HomeView'
const { turnPage } = usePage(router) </script>
|
跳转
跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| router.push('/users/eduardo')
router.push({ path: '/users/eduardo' })
router.push({ path: '/register', query: { plan: 'private' } })
router.push({ path: '/about', hash: '#team' })
router.push({ name: 'user', params: { username: 'eduardo' } })
|
替换
1 2 3
| router.push({ path: '/home', replace: true })
router.replace({ path: '/home' })
|
前进后退
1 2 3 4 5
| router.go(1)
router.go(-1)
|
参数
1 2 3 4 5
| import { useRoute } from 'vue-router' let route = useRoute()
route.query.userId; route.params.userId;
|
vuex
使用 useStore 来获取 store 对象 从 vuex 中取值时,要注意必须使用 computed 进行包装,这样 vuex 中状态修改后才能在页面中响应
1 2 3 4 5 6 7 8 9 10
| import {useStore} from 'vuex'
setup(){ const store = useStore() store.dispatch() store.commit() let category = computed(() => store.state.home.currentCagegory return { category } }
|
复用
在 Vue 3 中,混合(Mixins)依然是可用的,尽管 Vue 3 更加推荐使用 Composition API 来进行代码复用和管理。
使用 Mixins 的方法
定义混合对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export const myMixin = { data() { return { mixinData: 'Hello from mixin' }; }, methods: { mixinMethod() { console.log('This is a method from mixin'); } }, created() { console.log('Mixin created hook'); } };
|
在组件中使用混合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <div>{{ mixinData }}</div> </template>
<script> import { myMixin } from './myMixin';
export default { mixins: [myMixin], created() { this.mixinMethod(); console.log('Component created hook'); } }; </script>
|
使用 Composition API
使用 Composition API 替代 Mixins
在 Vue 3 中,推荐使用 Composition API 进行逻辑复用。通过 Composition API,你可以创建组合函数来封装逻辑,然后在多个组件中复用这些函数。以下是使用 Composition API 的一个示例:
定义组合函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { ref, onMounted } from 'vue';
export function useMixin() { const mixinData = ref('Hello from mixin');
const mixinMethod = () => { console.log('This is a method from mixin'); };
onMounted(() => { console.log('Mixin mounted hook'); });
return { mixinData, mixinMethod }; }
|
在组件中使用组合函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <div>{{ mixinData }}</div> </template>
<script> import { useMixin } from './useMixin';
export default { setup() { const { mixinData, mixinMethod } = useMixin(); mixinMethod(); console.log('Component setup hook');
return { mixinData }; } }; </script>
|
使用依赖注入
要为组件后代提供数据,需要使用到 provide() 函数:
1 2 3
| import { provide } from 'vue'
provide('loginUser', {name:'zhangsan'})
|
要注入上层组件提供的数据,需使用 inject() 函数:
1 2 3 4 5 6 7
| import { inject } from 'vue'
const loginUser: TUserInfo | undefined = inject('loginUser') let createid = 0 if (loginUser) { createid = loginUser.userid }
|
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
总结
虽然 Vue 3 仍然支持使用 Mixins,但 Vue 团队推荐使用 Composition API,因为它提供了更灵活、更可组合的方式来管理和复用逻辑,并且可以更好地处理复杂的组件逻辑和状态。