VUE3常用语法

VUE3 语法

setup

vue3 中用 setup 函数整合了所有的 api;只执行一次,在生命周期函数前执行,所以在 setup 函数中拿不到当前实例 this,不能用 this 来调用 vue2 写法中定义的方法

它将接受两个参数:props、context

1
2
3
4
5
6
// props - 组件接受到的属性 context - 上下文 
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 中使用

使用如下:

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() {
// ref
const eleList = ref([])
function addEle() {
let len = eleList.value.length
eleList.value.push({
id: len,
name: 'ref 自增' + len
})
}

// reactive
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>

computed

1
2
3
4
5
// computed
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
watch(eleList,(curVal, oldVal) => {
console.log('监听器:', curVal, oldVal)
})

深度监听

1
2
3
4
5
6
import { ref, watch } from 'vue'

// watch
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) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ 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)
// 当 count 的值被修改时,会执行回调
const stop = watchEffect(() => console.log(count.value))

// 停止监听
stop()

还可以停止监听,watchEffect 返回一个函数,执行后可以停止监听

与 vue2 一样:

1
2
3
4
const unwatch = this.$watch('say', curVal => {})

// 停止监听
unwatch()

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 // string
props.bar // number | undefined
</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) {
// 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 }
});

// 监听 sortId 的变化
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
<!-- Child.vue -->
<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
<!-- Parent.vue -->
<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
<!-- Child.vue -->
<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
<!-- Parent.vue -->
<Child v-model="foo" />

内部实际将被编译为:

1
2
3
4
5
<!-- Parent.vue -->
<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
<!-- CustomInput.vue -->
<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 生命周期钩子运行的,所以不需要显式地定义它们
换句话说,

想在beforeCreatecreated中编写的任何代码都应该直接在 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
import {useRoute, useRouter} from 'vue-router'

const route = useRoute() // 相当于 vue2 中的 this.$route
const router = useRouter() // 相当于 vue2 中的 this.$router

// route 用于获取当前路由数据
// router 用于路由跳转

跳转

跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 字符串路径
router.push('/users/eduardo')

// 带有路径的对象
router.push({ path: '/users/eduardo' })

// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })

// 命名的路由,并加上参数,让路由建立 url
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.forward() 相同
router.go(1)

// 返回一条记录,与 router.back() 相同
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() // 相当于 vue2 中的 this.$store
store.dispatch() // 通过 store 对象来 dispatch 派发异步任务
store.commit() // commit 修改 store 数据

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
// myMixin.js
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
// MyComponent.vue
<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
// useMixin.js
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
// MyComponent.vue
<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
4
5
<script setup>
import { provide } from 'vue'

provide('loginUser', {name:'zhangsan'})
</script>

要注入上层组件提供的数据,需使用 inject() 函数:

1
2
3
4
5
<script setup>
import { inject } from 'vue'

const loginUser = inject('loginUser')
</script>

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

总结

虽然 Vue 3 仍然支持使用 Mixins,但 Vue 团队推荐使用 Composition API,因为它提供了更灵活、更可组合的方式来管理和复用逻辑,并且可以更好地处理复杂的组件逻辑和状态。