组件基础
父组件
<script lang="ts">
import { defineComponent, ref, reactive, watch } from 'vue'
// 1. 导入组件
import MyProfile from './components/MyProfile.vue'
interface Person {
name: string
age: number
}
export default defineComponent({
name: 'App',
components: {
MyProfile
},
setup() {
const user: Person = reactive({
name: 'sunflow',
age: 18
})
return {
user,
}
}
})
</script>
<template>
<div>
<h2>{{ user.age }}</h2>
</div>
<!-- 2. 使用组件 -->
<MyProfile :user></MyProfile>
</template>
子组件
<template>
<div class="profile-component">
<h1>Name:{{ user.name }}</h1>
<h1>Age:{{ user.age }}</h1>
<h1>doubleName:{{ doubleAge }}</h1>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from 'vue'
interface Person {
name: string
age: number
}
export default defineComponent({
name: 'MyProfile',
// 外层有template了,不需要再这里再次定义!这里应该去掉template
template: `<div>{{ doubleAge }}</div>`,
// 如果没有使用 <script setup>,props 必须以 props 选项的方式声明,
// props 对象会作为 setup() 函数的第一个参数被传入:
props: {
user: {
// 引入PropType,通过泛型给Object做标注
type: Object as PropType<Person>,
required: true
},
age: {
type: Number,
required: true
}
},
setup(props) {
const doubleAge = computed(() => props.user.age * 2)
return { doubleAge }
}
})
</script>
<style lang="scss" scoped></style>
传递props
<script setup>
- **defineProps **是一个仅
<script setup>
中可用的编译宏命令,并不需要显式地导入 - 声明的 props 会自动暴露给模板
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>
<template>
<h4>{{ title }}</h4>
</template>
- defineProps 会返回一个对象
const props = defineProps(['title'])
console.log(props.title)
未设置setup
export default {
// 要用选项式声明
props: ['title'],
// props 对象会作为 setup() 函数的第一个参数
setup(props) {
console.log(props.title)
}
}
PropType<T>
用于在用运行时 props 声明时给一个 prop 标注更复杂的类型定义。
import type { PropType } from 'vue'
interface Book {
title: string
author: string
year: number
}
export default {
props: {
book: {
// 提供一个比 `Object` 更具体的类型
type: Object as PropType<Book>,
required: true
}
}
}
自定义事件$emit
基本
// 父组件
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
<!-- 子组件 -->
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
显示声明事件
我们可以通过 [defineEmits](https://cn.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
宏来声明需要抛出的事件: 在大型项目或TypeScript项目中,使用 defineEmits 可以帮助我们写出更清晰和更健壮的代码。 在使用TypeScript时对于类型检查特别有用,告诉其他开发者这个组件预计会触发哪些事件,增强了代码的可读性和可维护性
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
// 不加也可以,注意,defineEmits只能出现在setup
</script>
defineEmits 仅可用于 <script setup>
之中,并且不需要导入
// 返回一个等同于 $emit 方法的 emit 函数
<script setup>
const emit = defineEmits(['enlarge-text'])
// 可以被用于在组件的 <script setup> 中抛出事件
// 因为此处无法直接访问$emit
emit('enlarge-text')
</script>
未设置setup
如果没有在使用 <script setup>
,你可以通过 emits 选项定义组件会抛出的事件。 可以从 setup() 函数的第二个参数,即 setup 上下文对象上ctx
访问到 emit 函数
:
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
// ctx中取emit方法,发送给父组件
ctx.emit('enlarge-text')
}
}
类型标注
setup中
<script setup lang="ts">
// 运行时
const emit = defineEmits(['change', 'update'])
// 基于选项
const emit = defineEmits({
change: (id: number) => {
// 返回 `true` 或 `false`
// 表明验证通过或失败
},
update: (value: string) => {
// 返回 `true` 或 `false`
// 表明验证通过或失败
}
})
// 基于类型
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+: 可选的、更简洁的语法
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
</script>
未使用setup
import { defineComponent } from 'vue'
export default defineComponent({
emits: ['change'],
setup(props, { emit }) {
emit('change') // <-- 类型检查 / 自动补全
}
})
组合式函数
“组合式函数”(Composables) 是一个利用 Vue 的组合式 API
来封装和复用有状态逻辑的函数。
基础
hooks
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
const useMouse = () => {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
// 1.默认导出,引入的时候不加 {}
// export default useMouse
// 2.导出一个对象,导入的时候需要加{}解构
export { useMouse }
// 3. export function useMouse => 直接写,导入也要{}
组件中使用
// 如果不是script setup;需要return暴露出去
<script setup>
import { useMouse } from './hooks/useMouse'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
异步状态
useFetch举例
useFetch应当可以接收一个ref
const url = ref('/initial-url')
const { data, error } = useFetch(url)
// 这将会重新触发 fetch
url.value = '/new-url'
useFetch接收一个getter函数
// 当 props.id 改变时重新 fetch
const { data, error } = useFetch(() => `/posts/${props.id}`)
toValue() 是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值
。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数
// useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
// toValue
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
// watchEffect
watchEffect(() => {
fetchData()
})
return { data, error }
}
约定
组合式函数约定用驼峰命名法命名,并以“use”
作为开头。
toValue
利用 toValue() 工具函数,处理一下输入参数是 ref 或 getter
而非原始值的情况。 如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它能够被正确追踪,请确保要么使用 watch() 显式地监视 ref 或 getter,要么在 watchEffect() 中调用 toValue()。
返回值
我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象
,这样该对象在组件中
被解构为 ref 之后仍可以保持响应性
:
// x 和 y 是两个 ref
const { x, y } = useMouse()
// 如果从useMouse中直接返回响应式对象,会导致响应式连接丢失
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包,
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),
- 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:onMounted()
- 确保在 onUnmounted() 时清理副作用。
使用限制
组合式函数只能在 <script setup> 或 setup()
钩子中被调用,在这些上下文中,它们也只能被同步调用。
优势-Mixin
- 明确的数据来源
使用mixin时,数据、方法和计算属性被注入到组件中,有时很难追踪这些属性的来源。组合式API通过使用函数返回的值来组织逻辑,使得跟踪变量和函数的来源变得直截了当。
- 避免命名冲突
mixin可能会导致命名空间的冲突,尤其是在使用多个mixin时,不同的mixin之间可能会无意中使用相同的属性或方法名。而组合式API通过作用域隔离确保了命名冲突的问题不会发生。
- 更好的类型推断
TypeScript与组合式API协同工作得更好。由于组合式API是基于函数的,TypeScript能够提供更准确的类型推断和智能感知。而mixins在TypeScript中往往需要额外的工作来提供适当的类型信息。
- 逻辑重用与模块化
组合式API鼓励将逻辑片段封装到可复用的函数中,这使得代码更加模块化,同时函数也可以在不同组件间被引入和组合使用
。
- 代码可维护性
在大型项目中使用mixins可能导致理解和维护组件变得复杂。组合式API使得相关代码被组合在一起,更易于维护和迭代开发。
- Tree-shaking
如果一个组合式函数未被使用,现代JavaScript工具如Webpack可以通过tree-shaking排除这段未使用的代码,减小最终包的体积。而mixin则很难实现相同的优化。
- 更好的控制暴露的API
组合式函数只返回需要公开的响应性引用和函数,这让开发者能够更清晰控制组件的公共接口。