根据官方文档以及网上视频总结的基础vue知识,主要是方便自己查阅,以vue3组合式API为主
vue知识点梳理-1
11 模板引用
如果需要直接访问底层 DOM 元素,我们可以使用ref
<input ref="input">
ref
允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用
11.1 访问模板引用
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
<input ref="input" />
11.2 为模板引用标注类型(TS)
模板引用需要通过一个显式指定的泛型参数和一个初始值 null
来创建.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)
onMounted(() => {
el.value?.focus()
})
</script>
<template>
<input ref="el" />
</template>
注意为了严格的类型安全,有必要在访问 el.value
时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 ref
的值都是初始的 null
,并且在由于 v-if
的行为将引用的元素卸载时也可以被设置为 null
。
11.3 v-for
中的模板引用
当在 v-for
中使用模板引用时,对应的 ref
中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
应该注意的是,ref
数组并不保证与源数组相同的顺序。
11.4 函数模板引用
除了使用字符串值作名字,ref
还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null
。你当然也可以绑定一个组件方法而不是内联函数。
11.5 组件上的 ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例,这意味着父组件可以通过ref
直接获取到子组件的值
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
// child.value 是 <Child /> 组件的实例
})
</script>
<template>
<Child ref="child" />
</template>
注意:使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref
都会自动解包,和一般的实例一样)。
11.6 为组件模板引用标注类型(TS)
TypeScript 与组合式 API | Vue.js (vuejs.org)
12 深入组件
12.1 注册
12.2 props
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
除了使用字符串数组来声明 prop 外,还可以使用对象的形式:
// 使用 <script setup>
defineProps({
title: String,
likes: Number
})
对于以对象形式声明中的每个属性,key
是 prop
的名称,而值则是该prop
预期类型的构造函数。
使用一个对象绑定多个 prop
const post = {
id: 1,
title: 'My Journey with Vue'
}
以及下面的模板:
template
<BlogPost v-bind="post" />
而这实际上等价于
<BlogPost :id="post.id" :title="post.title" />
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:
js
const props = defineProps(['foo'])
// ❌ 警告!prop 是只读的!
props.foo = 'bar'
想要更改一个 prop 的需求通常来源于以下两种场景:
-
prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 const counter = ref(props.initialCounter)
-
需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
- 所有 prop 默认都是可选的,除非声明了
required: true
。 - 如果声明了
default
值,那么在 prop 的值被解析为undefined
时,无论 prop 是未被传递还是显式指明的undefined
,都会改为default
值。
13 事件
13.1 触发与监听事件
父组件可以通过 v-on
(缩写为 @
) 来监听事件:
<MyComponent @some-event="callback" />
同样,组件的事件监听器也支持 .once
修饰符:
<MyComponent @some-event.once="callback" />
13.2 声明触发的事件
组件可以显式地通过 defineEmits()
宏来声明它要触发的事件:
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
这个 emits
选项还支持对象语法,它允许我们对触发事件的参数进行验证:
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
如果你正在搭配 TypeScript 使用 <script setup>
,也可以使用纯类型标注来声明触发的事件:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
13.3 事件校验
和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit
的内容,返回一个布尔值来表明事件是否合法。
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
14 组件 v-model
v-model
可以在组件上使用以实现双向绑定。
<input v-model="searchText" />
在代码背后,模板编译器会对 v-model
进行更冗长的等价展开。因此上面的代码其实等价于下面这段:
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
而当使用在一个组件上时,v-model
会被展开为如下的形式:
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
要让这个例子实际工作起来,<CustomInput>
组件内部需要做两件事:
- 将内部原生
<input>
元素的value
attribute 绑定到modelValue
prop - 当原生的
input
事件触发时,触发一个携带了新值的update:modelValue
自定义事件
这里是相应的代码:
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
现在 v-model
可以在这个组件上正常工作了:
<CustomInput v-model="searchText" />
14.1 v-model
的参数
默认情况下,v-model
在组件上都是使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。我们可以通过给 v-model
指定一个参数来更改这些名字:
<MyComponent v-model:title="bookTitle" />
在这个例子中,子组件应声明一个 title
prop,并通过触发 update:title
事件更新父组件值:
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
14.2 多个 v-model
绑定
我们可以在单个组件实例上创建多个 v-model
双向绑定。
组件上的每一个 v-model
都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<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>
使用多个不同参数的 v-model
时使用修饰符:
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])
console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true}
</script>
15 属性透传
属性透传就是父组件定义的属性直接传入到子组件,有用的话会生效,无用的会当作自定义标签,透传过去的属性会和子组件进行合并,父组件的属性也会透传到儿子的儿子的上,事件也会透传,最常见的例子就是 class
、style
和 id
<!-- <MyButton> 的模板 -->
<button>click me</button>
一个父组件使用了这个组件,并且传入了 class
:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click me</button>
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
15.1 对 class
和 style
的合并
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton>
组件的模板改成这样:
<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
则最后渲染出的 DOM 结果会变成:
<button class="btn large">click me</button>
15.2 禁用 Attributes 继承
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
。
从 3.3 开始你也可以直接在 <script setup>
中使用 defineOptions
:
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
有时候我们可能为了样式,需要在 <button>
元素外包装一层 <div>
:
template
<div class="btn-wrapper">
<button class="btn">click me</button>
</div>
我们想要所有像 class
和 v-on
监听器这样的透传 attribute 都应用在内部的 <button>
上而不是外层的 <div>
上。我们可以通过设定 inheritAttrs: false
和使用 v-bind="$attrs"
来实现:
template
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
15.3 多根节点的 Attributes 继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs
没有被显式绑定,将会抛出一个运行时警告。
<CustomLayout id="custom-layout" @click="changeValue" />
如果 <CustomLayout>
有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
<header>...</header>
<main>...</main>
<footer>...</footer>
如果 $attrs
被显式绑定,则不会有警告:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
16 插槽
在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。就需要用到插槽
举例来说,这里有一个 <FancyButton>
组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton>
的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
- slot元素是一个插槽出口,标示了父元素提供的插槽内容将在哪里被渲染
- 插槽内容可以访问到父组件的数据作用域,因为插槽内容本身实在父组件模板中定义的
- 插槽内容无法访问子组件的数据。vue模板中的表达式只能访问其定义时所处的作用域
- 也就是说父组件模板中的表达式只能访问父组件的作用域 ,子组件模板中的表达式只能访问子组件的作用域
16.1 具名插槽
有时在一个组件中会包含多个插槽出口
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>
对于这种场景,<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
要为具名插槽传入内容,我们需要 v-slot
指令搭配 <template>
元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
16.2 作用域插槽
插槽的内容可能想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们需要一种方法来让子组件在渲染时将一部分数据提供给插槽。
可以像对组件传递 props
那样,向一个插槽的出口上传递 attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
我们也可以在 v-slot
中使用解构:
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
16.3 具名作用域插槽
插槽 props 可以作为 v-slot
指令的值被访问到:v-slot:name="slotProps"
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
template
<slot name="header" message="hello"></slot>
注意插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps
的结果是 { message: 'hello' }
。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。
为默认插槽使用显式的 <template>
标签有助于更清晰地指出 message
属性在其他插槽中不可用:
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>
16.4 使用作用域插槽示例
将对单个列表元素内容和样式的控制权留给使用它的父组件。
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
在 <FancyList>
之中,我们可以多次渲染 <slot>
并每次都提供不同的数据 (注意我们这里使用了 v-bind
来传递插槽的 props):
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
17 依赖注入
provide
和 inject
可以帮助我们,直接把父组件的数据传递到任意的子组件中去,无视逐级透传
17.1 Provide (提供)
要为组件后代提供数据,需要使用到 provide()
函数:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
provide()
函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol
。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
17.2 Inject (注入)
要注入上层组件提供的数据,需使用 inject()
函数:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly()
来包装提供的值。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
18 组合式函数
组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
18.1 鼠标跟踪器示例
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
js
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
下面是它在组件中使用的方式:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
useMouse()
的功能可以在任何组件中轻易复用了。
19 自定义指令
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 还允许你注册自定义的指令。
下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
vue
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus
即可以在模板中以 v-focus
的形式使用。
将一个自定义指令全局注册到应用层级也是一种常见的做法:
js
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。
19.1 指令钩子
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
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
钩子中可用。
举例来说,像下面这样使用指令:
<div v-example:foo.bar="baz">
binding
参数会是一个这样的对象:
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
<div v-example:[arg]="value"></div>
这里指令的参数会基于组件的 arg
数据属性响应式地更新。
对于自定义指令来说,一个很常见的情况是仅仅需要在 mounted
和 updated
上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
template
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
如果你的指令需要多个值,你可以向它传递一个 JavaScript 对象字面量。别忘了,指令也可以接收任何合法的 JavaScript 表达式。
template
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
js
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
不推荐在组件上使用自定义指令。