Vue 3.0
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,它采用组件化和响应式数据绑定的方式,简化了 Web 应用程序的开发。
Vue3 核心
new Proxy()
Proxy 对象用于创建一个对象的代理(和原始对象不相等),从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。这是 Vue3 核心底层逻辑。
语法
const vm = new Proxy(target, handler);
示例
const target = {
name: "John",
age: 30,
};
const proxy = new Proxy(target, {
get(target, property) {
return target[property];
},
set(target, property, value) {
target[property] = value;
},
});
插值语法
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
<span>Message: {{ msg }}</span>
指令语法
指令是带有 v- 前缀的特殊 attribute。
v-text
v-text 通过设置元素的 textContent 属性来工作,因此它将覆盖元素中所有现有的内容。
<span v-text="msg"></span>
<!-- 等同于 -->
<span>{{msg}}</span>
v-html
v-html 的内容直接作为普通 HTML 插入(Vue 模板语法是不会被解析的) 。
<div v-html="html"></div>
TIP
在单文件组件,scoped 样式将不会作用于 v-html 里的内容,因为 HTML 内容不会被 Vue 的模板编译器解析。如果你想让 v-html 的内容也支持 scoped CSS,你可以使用 CSS modules 或使用一个额外的全局 <style>
元素,手动设置类似 BEM 的作用域策略。
v-bind (简写为 ":"
)
动态的绑定一个或多个 attribute,也可以是组件的 prop。
<!-- 绑定 attribute -->
<img v-bind:src="imageSrc" />
<!-- 动态 attribute 名 -->
<button v-bind:[key]="value"></button>
<!-- 缩写 -->
<img :src="imageSrc" />
<!-- 缩写形式的动态 attribute 名 -->
<button :[key]="value"></button>
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 绑定对象形式的 attribute -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- prop 绑定。“prop” 必须在子组件中已声明。 -->
<MyComponent :prop="someThing" />
<!-- 传递子父组件共有的 prop -->
<MyComponent v-bind="$props" />
修饰符
- .camel:将短横线命名的 attribute 转变为驼峰式命名
- .prop:强制绑定为 DOM property
- .attr:强制绑定为 DOM attribute
v-model
在表单输入元素或组件上创建双向绑定。
<!--文本-->
<input v-model="message" placeholder="edit me" />
<!--多行文本-->
<textarea v-model="message" placeholder="add multiple lines"></textarea>
<!--复选框-->
<input type="checkbox" id="checkbox" v-model="toggle" true-value="yes" false-value="no" />
<input type="checkbox" id="checkbox" v-model="toggle" :true-value="dynamicTrueValue" :false-value="dynamicFalseValue" />
<!--单选按钮-->
<input type="radio" id="radio" v-model="picked" :value="first" />
<!--选择器-->
<select v-model="selected">
<option disabled :value="">Please select one</option>
<option :value="A">A</option>
<option :value="B">B</option>
<option :value="C">C</option>
</select>
仅限:
<input>
<select>
<textarea>
- components
修饰符
- .lazy:监听 change 事件而不是 input
- .number:将输入的合法字符串转为数字
- .trim:移除输入内容两端空格
v-if
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
<template>
上的 v-if
在一个 <template>
元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素。
v-show
基于表达式值的真假性,来改变元素的可见性。
<h1 v-show="ok">Hello!</h1>
TIP
- v-show 会在 DOM 渲染中保留该元素
- v-show 仅切换了该元素上名为 display 的 CSS 属性
- v-show 不支持在
<template>
元素上使用,也不能和 v-else 搭配使用
v-for
基于原始数据多次渲染元素或模板块。
<div v-for="item in items" :key="item.id"></div>
<div v-for="(item, index) in items" :key="index"></div>
<div v-for="(value, key) in object" :key="key"></div>
<div v-for="(value, name, index) in object" :key="index"></div>
<template>
上的 v-for
在一个 <template>
元素上使用 v-for,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template>
元素。
v-for 与 v-if
当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高,需要在外新包装一层 <template>
再在其上使用 v-for 解决v-if 的条件将无法访问到 v-for 作用域内定义的变量别名问题。
v-on (简写为 "@"
)
给元素绑定事件监听器。
<script setup>
const count = ref(0)
function greet() {
alert(count.value)
}
function say(message) {
alert(message)
}
function warn(message, event) {
if (event) {
event.preventDefault()
}
alert(message)
}
</script>
<template>
<!--内联事件-->
<button @click="count++">Add</button>
<!--方法事件-->
<button @click="greet">Greet</button>
<!--方法事件传参-->
<button @click="say('hello')">Say hello</button>
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">Submit</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">Submit</button>
</template>
事件修饰符
.stop
:修饰符用于停止事件冒泡,阻止事件进一步传播到父元素。.prevent
:修饰符用于阻止事件的默认行为,例如阻止表单提交或链接跳转。.self
:修饰符用于限制事件只能在触发该事件的元素本身上执行,而不包括其子元素。.capture
:修饰符用于将事件监听器添加到捕获阶段,而不是冒泡阶段。.once
:修饰符用于只绑定一次事件监听器,在事件触发后自动解绑监听器。.passive
:修饰符用于告知浏览器该事件监听器不会调用 preventDefault(),从而提高滚动性能。
按键修饰符
.enter
:修饰符用于处理回车键(Enter)的按下事件。.tab
:修饰符用于处理制表键(Tab)的按下事件。.delete
:修饰符用于处理删除键(Backspace 或 Delete)的按下事件。.esc
:修饰符用于处理逃逸键(Escape)的按下事件。.space
:修饰符用于处理空格键(Space)的按下事件。.up
:修饰符用于处理上箭头键(Up Arrow)的按下事件。.down
:修饰符用于处理下箭头键(Down Arrow)的按下事件。.left
:修饰符用于处理左箭头键(Left Arrow)的按下事件。.right
:修饰符用于处理右箭头键(Right Arrow)的按下事件。.ctrl
:修饰符用于处理 Ctrl 键的按下事件。.alt
:修饰符用于处理 Alt 键的按下事件。.shift
:修饰符用于处理 Shift 键的按下事件。.meta
:修饰符用于处理 Meta 键的按下事件。在不同的操作系统中,Meta 键可能对应于 Command 键(⌘)或 Windows 键。.exact
:修饰符用于确切匹配按键事件,要求事件的修饰符完全匹配,而不是部分匹配。
鼠标按键修饰符
.left
:修饰符用于处理鼠标左键的点击事件。.right
: 修饰符用于处理鼠标右键的点击事件。.middle
:修饰符用于处理鼠标中键的点击事件。
v-slot(简写为 "#"
)
用于声明具名插槽或是期望接收 props 的作用域插槽。
仅限:
<template>
- components (用于带有 prop 的单个默认插槽)
v-pre
跳过该元素及其所有子元素的编译。
<span v-pre>{{ this will not be compiled }}</span>
v-once
仅渲染元素和组件一次,并跳过之后的更新。
<span v-once>This will never change: {{msg}}</span>
v-memo
缓存一个模板的子树。
<div v-memo="[valueA, valueB]">
...
</div>
与 v-for 一起使用
当搭配 v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。
v-cloak
用于隐藏尚未完成编译的 DOM 模板。
[v-cloak] {
display: none;
}
<div v-cloak>
{{ message }}
</div>
自定义指令
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
指令钩子
const myDirective = {
created(el, binding, vnode, prevVnode) {},
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) {}
}
TIP
除了 el 外,其他参数都是只读的,不要更改它们。若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现。
局部指令
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
TIP
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
全局指令
app.directive()
// 注册(对象形式的指令)
app.directive('my-directive', {
/* 自定义指令钩子 */
})
// 注册(函数形式的指令)
app.directive('my-directive', (el, binding) => {
/* ... */
})
// 得到一个已注册的指令
const myDirective = app.directive('my-directive')
不推荐在组件上使用自定义指令
当在组件上使用自定义指令时,会始终应用于组件的根节点,当应用到一个多根组件时,指令将会被忽略且抛出一个警告。且不能通过 v-bind="$attrs" 来传递给一个不同的元素。
Class 与 Style 绑定
Class 绑定
绑定对象
<script setup>
const isActive = ref(true);
const hasError = ref(true);
const classObject = reactive({
active: true,
"text-danger": false,
});
const classObjectComputed = computed(() => ({
active: isActive.value && !hasError.value,
"text-danger": hasError.value && hasError.value.type === "false",
}));
</script>
<div :class="{ active: isActive }"></div>
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
<div :class="classObject"></div>
<div :class="classObjectComputed"></div>
绑定数组
<script setup>
const activeClass = ref("active");
const errorClass = ref("text-danger");
</script>
<div :class="[activeClass, errorClass]"></div>
Style 绑定
绑定对象
<script setup>
const activeColor = ref("red");
const fontSize = ref(30);
const styleObject = reactive({
color: "red",
fontSize: "13px",
});
</script>
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="{ 'font-size': fontSize + 'px' }"></div>
<div :style="styleObject"></div>
绑定数组
<div :style="[baseStyles, overridingStyles]"></div>
组件作用域 scoped
当 <style>
标签带有 scoped
的时候,它的 CSS 只会影响当前组件的元素。
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
注意
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
深度选择器
处于 scoped 样式中的选择器如果想要做更“深度”的选择,也即:影响到子组件,可以使用 :deep() 这个伪类:
<style scoped>
.a :deep(.b) {
/* ... */
}
</style>
插槽选择器
默认情况下,作用域样式不会影响到 <slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted 伪类以明确地将插槽内容作为选择器的目标:
<style scoped>
:slotted(div) {
color: red;
}
</style>
全局选择器
如果想让其中一个样式规则应用到全局,比起另外创建一个 <style>
,可以使用 :global 伪类来实现:
<style scoped>
:global(.red) {
color: red;
}
</style>
混合使用局部与全局样式
在同一个组件中同时包含作用域样式和非作用域样式:
<style>
/* 全局样式 */
</style>
<style scoped>
/* 局部样式 */
</style>
模块化 module
一个 <style module>
标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style
对象暴露给组件:
<template>
<p :class="$style.red">This should be red</p>
</template>
<style module>
.red {
color: red;
}
</style>
自定义注入名称
<template>
<p :class="classes.red">red</p>
</template>
<style module="classes">
.red {
color: red;
}
</style>
与组合式 API 一同使用
可以通过 useCssModule API 在 setup() 和 <script setup>
中访问注入的 class。对于使用了自定义注入名称的 <style module>
块,useCssModule 接收一个匹配的 module 属性值作为第一个参数:
import { useCssModule } from "vue";
// 在 setup() 作用域中...
// 默认情况下, 返回 <style module> 的 class
useCssModule();
// 具名情况下, 返回 <style module="classes"> 的 class
useCssModule("classes");
内联样式 v-bind()
<script setup>
const theme = {
color: "red",
};
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind("theme.color");
}
</style>
组合式 API
setup()
setup() 钩子是使用Vue组件的入口。且本教程仅讨论组合式 API 语法糖 <script setup>
风格偏好。
<script setup>
基本语法
在 <script>
代码块上添加 setup attribute:
<script setup>
const message = 'this is message';
const logMessage = () => {
console.log(message);
}
</script>
<template>
<div>
{{message}}
<button @click="logMessage">console.log</button>
</div>
</template>
defineOptions()
用来直接在 <script setup>
中声明组件选项,而不必使用单独的 <script>
块。
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
}
})
</script>
await
<script setup>
中可以使用顶层 await。结果代码会被编译成 async setup():
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
WARNING
async setup() 必须与 Suspense 内置组件组合使用。
响应式 API
ref 全家桶
ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
<script setup>
import { ref } from "vue";
const count = ref(0);
const addCount = () => {
count.value++;
};
</script>
<template>
<button @click="addCount">{{count}}</button>
</template>
TIP
ref 与 reactive 区别
- reactive 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效
- ref 创建可以使用任何类型,但是在
<script>
中必须通过.value
访问修改,在<template>
和被嵌套在reactive()
中会自动则不需要通过.value
访问修改 - ref 创建的对象类型重新赋值时依然具有响应式,而reactive会失去响应式
- ref 函数的内部实现依赖于 reactive 函数
ref 与 reactive 使用原则
- 若需要一个基本类型的响应式数据,必须使用
ref
- 若需要一个响应式对象且层级不深,
ref
与reactive
都可以 - 若需要一个响应式对象且层级较深,推荐使用
reactive
- 若需要一个基本类型的响应式数据,必须使用
isRef()
检查某个值是否为 ref。
toRef()
可以将值、refs 或 getters 规范化为 refs (3.3+),也可以基于 reactive 对象上的一个属性,创建一个对应的 ref。
let person = reactive({name: 'zs', age: 18})
let age = toRef(person, 'age')
console.log(age) // output: 18
toRefs()
将一个 reactive 对象转换为一个 ref 对象,这个 ref 对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
TIP
当从组合式函数中返回 reactive 对象时,toRefs 可以解构返回的对象而不会失去响应性。
let person = reactive({name: 'zs', age: 18})
let {name, age} = toRefs(person)
unref()
如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
计算的一个语法糖。
shallowRef()
ref() 的浅层作用形式。
triggerRef()
强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。
customRef()
import { customRef } from 'vue'
export function useCustomRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
<script setup>
import { useCustomRef } from './debouncedRef'
const text = useCustomRef('hello')
</script>
<template>
<input v-model="text" />
</template>
reactive 全家桶
reactive()
只接受对象类型数据(对象、数组和 Map、Set 这样的集合类型)的参数传入,返回一个深层响应式对象的代理。
<script setup>
import {reactive} from 'vue';
const state = reactive({
count:0;
})
const addCount = () => {
state.count++;
}
</script>
<template>
<button @click="addCount">{{state.count}}</button>
</template>
TIP
reactive 重新赋值一个新对象时,会失去响应式。解决方案:
Object.assign(currentObject, newObject)
{...currentObject, ...newObject}
- 将 reactive 转换成 ref 类型
isReactive()
检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。
shallowReactive()
reactive() 的浅层作用形式。
TIP
一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
readonly 全家桶
readonly()
接受一个 reactive 或是一个 ref,返回一个原值的只读代理。
isReadonly()
检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理。
shallowReadonly()
readonly() 的浅层作用形式。
TIP
只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
computed()
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
只读(默认)
<script setup>
import { reactive, computed } from "vue";
const author = reactive({
name: "John Doe",
books: [
"Vue 2 - Advanced Guide",
"Vue 3 - Basic Guide",
"Vue 4 - The Mystery",
],
});
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
author.books.length > 0 ? "Yes" : "No";
});
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
可读可写(getter & setter)
import { ref, computed } from "vue";
const firstName = ref("John");
const lastName = ref("Doe");
const fullName = computed({
// getter
get() {
return firstName.value + " " + lastName.value;
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(" ");
},
});
TIP
- computed() 通常用于描述依赖响应式状态的复杂逻辑处理
- computed() 的 getter 应只做计算和返回该值而没有任何其他的副作用,这一点非常重要,请务必牢记
- computed() 的返回值应该被视为只读的,避免直接修改计算属性值
watch 全家桶
watch()
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
watch(source, callback[, option])
TIP
- source:“监听源”,可以是一个 ref (包括计算属性)、一个 reactive 对象、一个 getter 函数、或多个数据源组成的数组
- callback:回调函数,每次响应式状态发生变化时触发,接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
- option:可选的参数是一个对象,支持以下这些选项:
- immediate:在侦听器创建时立即触发回调,第一次调用时旧值是 undefined
- deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调
- flush:调整回调函数的刷新时机
const x = ref(0);
const y = ref(0);
const obj = reactive({ count: 0 });
// 情况一:ref 基本类型
watch(x, (newX) => {
console.log(`x is ${newX}`);
});
// 情况二:reactive
watch(obj, (newCount, oldCount) => {
console.log("Count changed:", newCount, oldCount);
});
// 情况三:reactive 单个属性值
watch(
() => obj.count,
(newCount, oldCount) => {
console.log("Count changed:", newCount, oldCount);
}
);
// 情况四:getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`);
}
);
// 情况五:多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`);
});
立即侦听
const count = ref(0);
watch(
count,
(count, prevCount) => {
/* ... */
},
{
immediate: true,
}
);
注意
侦听器执行了 2 次,一次是作为源,另一次是在回调中。
深度侦听
const z = ref({z: 0})
const obj = reactive({ sum: {s1: 0, s2: 1} });
// ref 对象类型
watch(z, (newZ) => {
console.log(`z is ${newZ}`)
},{
deep: true // 若不开启深度监听,则只会监听内存地址变化,而不是对象属性。
})
// reactive 属性对象
watch(
() => obj.sum,
(newSum)=>{
console.log(`sum value is changed: ${newSum}`)
},{
deep: true //若不开启深度监听,则只会监听对象中的单个属性变化,而不是整个属性对象被替换
})
停止侦听器
const stop = watch(source, callback);
// 当已不再需要该侦听器时:
stop();
副作用清理
watch(id, async (newId, oldId, onCleanup) => {
const { response, cancel } = doAsyncWork(newId);
// 当 `id` 变化时,`cancel` 将被调用,
// 取消之前的未完成的请求
onCleanup(cancel);
data.value = await response;
});
侦听时机
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项:
watch(source, callback, {
flush: "post",
});
watchEffect()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行(即:不用明确指出监视的数据,函数中用到哪些属性就监视哪些属性)。
watchEffect(callback[, option])
TIP
- callback:回调函数,每次响应式状态发生变化时触发,接受一个参数:用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求
- option:可选的参数是一个对象,支持以下这些选项:
- flush:调整回调函数的刷新时机
- onTrack / onTrigger:调试侦听器的依赖
watchPostEffect()
watchEffect() 使用 flush: 'post' 选项时的别名。
watchSyncEffect()
watchEffect() 使用 flush: 'sync' 选项时的别名。
生命周期钩子
- onBeforeMount():在组件被挂载之前被调用
- onMounted():在组件挂载完成后执行
- onBeforeUpdate():在组件即将因为响应式状态变更而更新其 DOM 树之前调用
- onUpdated():在组件因为响应式状态变更而更新其 DOM 树之后调用
- onBeforeUnmount():在组件实例被卸载之前调用
- onUnmounted():在组件实例被卸载之后调用
- onErrorCaptured():注册一个钩子,在捕获了后代组件传递的错误时调用
- onActivated():注册一个回调函数,若组件实例是
<KeepAlive>
缓存树的一部分,当组件被插入到 DOM 中时调用 - onDeactivated():注册一个回调函数,若组件实例是
<KeepAlive>
缓存树的一部分,当组件从 DOM 中被移除时调用
依赖注入
- provide():提供一个值,可以被后代组件注入
- inject():注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值
模板引用 ref
用于注册模板引用,获取DOM元素。
<script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
v-for 模板
<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>
函数模板
<button :ref="el => { console.log(el) }"></button>
组件模板
<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>
单文件组件
组件是主要的构建模块。
单文件组件
在单文件组件和内联字符串模板中,推荐为子组件使用 PascalCase
的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。使用 />
来关闭一个标签,<PascalCase />
在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。
自动名称推导
SFC 在以下场景中会根据文件名自动推导其组件名:
- 开发警告信息中需要格式化组件名时;
- DevTools 中观察组件时;
- 递归组件自引用时。例如一个名为
FooBar.vue
的组件可以在模板中通过<FooBar/>
引用自己。(同名情况下) 这比明确注册/导入的组件优先级低。
全局组件
app.component()
如果同时传递一个组件名字符串及其定义,则注册一个全局组件;如果只传递一个名字,则会返回用该名字注册的组件 (如果存在的话)。
import { createApp } from 'vue';
import MyComponent from './MyComponent.vue';
const app = createApp();
app.component('my-component', MyComponent);
app.mount('#app');
批量注册全局组件
Object.entries(globalComponents).forEach(([name, component]) => {
app.component(name, component);
});
DANGER
全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
局部组件
在使用 <script setup>
的单文件组件中,直接 import 的组件(称为“局部组件”)即可直接在模板中使用,无需注册。
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
DANGER
- 局部组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好
- 局部组件在后代组件中并不可用
命名空间组件
可以使用带 . 的组件标签,例如 <Foo.Bar>
来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中直接用 <FooBar/>
引用它自己,无需 import 引入。
注意
直接引用单文件组件名相比于导入的组件优先级更低。如果有具名的导入和组件自身推导的名字冲突了,可以为导入的组件添加别名:
import { FooBar as FooBarChild } from './components'
动态组件
<component>
& is
由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup
> 中要使用动态组件的时候,应该使用动态的 :is
来绑定:
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
懒加载组件
<component :is="() => import('./Foo.vue')"></component>
缓存组件
<KeepAlive>
多个组件间动态切换时缓存被移除的组件实例。
<KeepAlive>
<component :is="someCondition ? Foo : Bar" />
</KeepAlive>
include & exclude
<KeepAlive>
默认会缓存内部的所有组件实例,但我们可以通过 include 和 exclude prop 来定制该行为。这两个 prop 的值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组:
<!-- 以英文逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="Foo" />
</KeepAlive>
<!-- 正则表达式 (需使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="Foo" />
</KeepAlive>
<!-- 数组 (需使用 `v-bind`) -->
<KeepAlive :exclude="['a', 'b']">
<component :is="Foo" />
</KeepAlive>
max
限制可被缓存的最大组件实例数。
<KeepAlive :max="10">
<component :is="Foo" />
</KeepAlive>
异步组件
defineAsyncComponent()
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
全局注册
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
<Suspense>
(实验性功能)
在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
占位符组件
<template>
当使用v-if
、v-for
、v-slot
指令而不在 DOM 中渲染元素时,<template>
标签可以作为占位符使用。
TIP
单文件组件使用顶层的 <template>
标签来包裹整个模板。这种用法与上面描述的 <template>
使用方式是有区别的。该顶层标签不是模板本身的一部分,不支持指令等模板语法。
传送组件
<Teleport>
将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
过渡动画组件
<Transition>
在一个元素或组件进入和离开 DOM 时应用动画。
触发条件
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素
<component>
切换的动态组件 - 改变特殊的 key 属性
CSS 过渡 class
- v-enter-from:进入动画的起始状态
- v-enter-active:进入动画的生效状态
- v-enter-to:进入动画的结束状态
- v-leave-from:离开动画的起始状态
- v-leave-active:离开动画的生效状态
- v-leave-to:离开动画的结束状态
name
对于一个有名字的过渡效果,过渡 class 会以 name 而不是 v 作为前缀。
<Transition name="fade">
...
</Transition>
自定义过渡 class
对于使用第三方动画库的过渡效果,过渡 class 会引入第三方动画 class 实现。
- enter-from-class
- enter-active-class
- enter-to-class
- leave-from-class
- leave-active-class
- leave-to-class
<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
name="custom-classes"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
...
</Transition>
钩子函数
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<!-- ... -->
</Transition>
function onBeforeEnter(el) {}
function onEnter(el, done) {
done()
}
function onAfterEnter(el) {}
function onEnterCancelled(el) {}
function onBeforeLeave(el) {}
function onLeave(el, done) {
done()
}
function onAfterLeave(el) {}
function onLeaveCancelled(el) {}
TIP
在使用仅由 JavaScript 执行的动画时,最好是添加一个 :css="false" prop。这显式地向 Vue 表明可以跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡效果。且对于 @enter
和 @leave
钩子来说,回调函数 done
就是必须的。否则,钩子将被同步调用,过渡将立即完成。
封装
<!-- MyTransition.vue -->
<script>
// JavaScript 钩子逻辑...
</script>
<template>
<!-- 包装内置的 Transition 组件 -->
<Transition
name="my-transition"
@enter="onEnter"
@leave="onLeave">
<slot></slot> <!-- 向内传递插槽内容 -->
</Transition>
</template>
<style>
/*
必要的 CSS...
注意:避免在这里使用 <style scoped>
因为那不会应用到插槽内容上
*/
</style>
appear
<Transition appear>
...
</Transition>
mode
<Transition mode="out-in">
...
</Transition>
<TransitionGroup>
在一个 v-for 列表中的元素或组件被插入,移动,或移除时应用动画。
与 <Transition>
的区别
- 默认情况下,它不会渲染一个容器元素。但你可以通过传入
tag
prop 来指定一个元素作为容器元素来渲染 - 过渡模式在这里不可用,因为我们不再是在互斥的元素之间进行切换
- 列表中的每个元素都必须有一个独一无二的
key
attribute - CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上
组件通信方式
父传子(Props)
Prop 名字格式
父组件在向子组件传递 props 时,应使用 kebab-case
形式命名,而子组件在接收父组件传递 props 时,应使用 camelCase
形式命名。
defineProps()
只能在使用 <script setup>
的单文件组件中,子组件在接收父组件传递 props 可以使用 defineProps() 宏来声明:
- 字符串数组形式
<script setup>
import { defineProps } from 'vue';
const props = defineProps(['foo']);
</script>
- 对象字面量形式(必须指定类型)
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
title: String,
likes: Number
})
</script>
注意
- 在
<script setup>
中访问 defineProps() 声明的对象,必须使用props.foo
- 在
<template>
中访问 defineProps() 声明的对象,可以推荐使用foo
,也要使用props.foo
- defineProps 接收与 props 选项相同的值
Prop 传值
- 静态值
<BlogPost title="My journey with Vue" />
- 动态值
<!-- 根据一个变量的值动态传入 -->
<BlogPost :title="post.title" />
<!-- 根据一个更复杂表达式的值动态传入 -->
<BlogPost :title="post.title + ' by ' + post.author.name" />
Prop 校验
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})
示例
<!--父组件-->
<script setup>
import { ref } from "vue";
import SonCom from "./son-com.vue";
const count = ref(10);
</script>
<template>
<div class="father">
<h2>父组件App</h2>
<SonCom :count="count" get-message="father message" />
</div>
</template>
<!--子组件 son-com.vue-->
<script setup>
const props = defineProps({
count: Number,
getMessage: String,
});
console.log(props);
</script>
<template>
<div class="son">
<h2>子组件Son</h2>
<p>父组件传入的数据{{getMessage}}:{{count}}</p>
</div>
</template>
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
子传父(Emit, 自定义事件)
Emit 名字格式
子组件声明的事件名以 camelCase
形式来触发,父组件声明的事件名以 kebab-case
形式来监听。
defineEmits()
只能在子组件 <script setup>
中显式地通过 defineEmits()
宏函数来声明要触发的事件:
- 字符串数组形式
<script setup>
const emit = defineEmits(['submit']);
const handler = () => {
emit('submit');
}
</script>
- 对象字面量形式
<script>
const emit = defineEmits({
inFocus() {
console.log('inFocus event triggered');
},
submit(payload) {
console.log('submit event triggered with payload:', payload);
}
});
const handler = () => {
emit('inFocus');
}
const buttonClick = (customPayload) => {
emit('submit', customPayload);
}
</script>
注意
- 在
<script setup>
中访问defineEmits()
声明的对象,必须使用emit('submit')
- 在
<template>
中访问defineEmits()
声明的对象(<script setup>
中没有const emit = defineEmits()
情况下),可以使用$emit('submit')
- defineEmits 接收与 emits 选项相同的值
emit
在组件的<template>
中,可以直接使用 $emit()
方法触发自定义事件。
<button @click="$emit('someEvent')">click me</button>
TIP
所有传入 emit()
的额外参数都会被直接传向监听器。如 emit('foo', 1, 2, 3)
触发后,监听器函数将会收到这三个参数值。
示例
<!--父组件-->
<script setup>
import SonCom from "./son-com.vue";
const getHandler = (msg) => {
console.log(msg);
};
</script>
<template>
<div class="father">
<h2>父组件App</h2>
<SonCom @send-message="getHandler" />
</div>
</template>
<!--子组件 son-com.vue-->
<script setup>
const emit = defineEmits(["sendMessage"]);
const sendMsg = (msg) => {
emit("sendMessage", msg);
};
</script>
<template>
<div class="son">
<h2>子组件Son</h2>
<button @click="sendMsg('this is son message')">触发子组件事件</button>
</div>
</template>
父子互传(v-model)
v-model
在子组件上使用以实现父子组件双向绑定。
<MyComponent v-model="bookTitle" />
<!--等价于-->
<MyComponent
:modelValue="bookTitle"
@update:modelValue="newValue => bookTitle = newValue"
></MyComponent>
自定义 prop
<MyComponent v-model:title="bookTitle" />
<!--等价于-->
<MyComponent
:title="bookTitle"
@update:title="newValue => bookTitle = newValue"
></MyComponent>
defineModel()
待更新
示例
<!--父组件-->
<script setup>
import { ref } from "vue";
import CustomInput from "./CustomInput.vue";
const message = ref("hello1");
</script>
<template>
<CustomInput v-model="message" />
</template>
<!--子组件 CustomInput-->
<script setup>
defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
const inputHandler = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="inputHandler" />
</template>
<!--等价于-->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
父子互传(defineExpose)
defineExpose()
使用 <script setup>
的组件是默认关闭的——即通过模板引用(ref)或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
可以通过 defineExpose 编译器宏来显式指定在 <script setup>
组件中要暴露出去的属性:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
ref
父组件获取子组件实例暴露出来的属性。
$refs
父组件获取所有子组件实例暴露出来的属性。
$parent
子组件获取父组件实例暴露出来的属性。
示例
<!--父组件-->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
let fatherNum = ref(10000),
childRef= ref();
const handler = () => {
fatherNum.value += 10
childRef.value.childNum -= 10
}
defineExpose({ fatherNum })
</script>
<template>
<h1>我是父组件:{{ fatherNum }}</h1>
<Child ref="childRef" />
<button @click="handler">父组件加10,子组件减10</button>
</template>
<!--子组件1-->
<script setup>
import { ref } from 'vue'
const childNum = ref(100)
const doubleNum = ($parent) => {
childNum.value *= 2
$parent.fatherNum /= 2
}
defineExpose({
childNum
})
</script>
<template>
<h2>我是子组件:{{ childNum }}</h2>
<button @click="doubleNum($parent)">子组件翻倍,父组件减倍</button>
</template>
兄弟互传(事件总线)
在 Vue 3 中,事件总线的概念已经被移除,但仍然可以使用自定义的全局事件总线实现组件间的通信。
父组件代理
<!--父组件-->
<script setup>
import A from './A.vue'
import B from './B.vue'
import { ref } from 'vue'
let Flag = ref(false)
const getFlagA = flag => {
Flag.value = flag;
}
const getFlagB = flag => {
Flag.value = flag
}
</script>
<template>
<div>
<A @on-click="getFlagA" :is-flag="Flag"></A>
<br />
<B @on-click="getFlagB" :is-flag="Flag"></B>
</div>
</template>
<!--子组件A-->
<script setup>
const emit = defineEmits(['onClick'])
const props = defineProps(['isFlag'])
const handler = (flag) => {
emit('onClick',flag)
}
</script>
<template>
<button @click="handler(!isFlag)">A组件Flag状态为:{{isFlag}}</button>
</template>
<!--子组件B-->
<script setup>
const emit = defineEmits(['onClick'])
const props = defineProps(['isFlag'])
const handler = flag => {
emit('onClick',flag)
}
</script>
<template>
<button @click="handler(!isFlag)">B组件Flag状态为:{{isFlag}}</button>
</template>
自定义事件总线
// EventBus.js
import { reactive, readonly } from "vue";
const eventBus = reactive({});
function $on(eventName, callback) {
if (!eventBus[eventName]) {
eventBus[eventName] = [];
}
eventBus[eventName].push(callback);
}
function $emit(eventName, ...args) {
if (eventBus[eventName]) {
eventBus[eventName].forEach((callback) => callback(...args));
}
}
const EventBus = readonly({
$on,
$emit,
});
export default EventBus;
<!--App.vue-->
<script setup>
import A from './components/A.vue'
import B from './components/B.vue'
</script>
<template>
<div>
<A />
<B />
</div>
</template>
<!--A.vue-->
<script setup>
import EventBus from "../bus/Bus.js";
import { ref } from 'vue';
let sayB = ref(null)
EventBus.$on('eventA', data => {
console.log('Received eventA:', data);
sayB.value = data;
});
function handleClick() {
EventBus.$emit('eventB', 'Hello from ComponentA');
};
</script>
<template>
<div>
<h1>B组件对A组件说:{{ sayB }}</h1>
<button @click="handleClick">发送给B</button>
</div>
</template>
<!--B.vue-->
<script setup>
import EventBus from "../bus/Bus.js";
import { ref } from 'vue';
let sayA = ref(null);
EventBus.$on('eventB', data => {
console.log('Received eventB:', data);
sayA.value = data;
})
function handleClick() {
EventBus.$emit('eventA', 'Hello from ComponentB');
}
</script>
<template>
<div>
<h1>A组件对B组件说:{{ sayA }}</h1>
<button @click="handleClick">发送给A</button>
</div>
</template>
第三方库 Mitt
//EventBus.js
import mitt from "mitt";
const bus = mitt();
export default bus;
<!--App.vue-->
<script setup>
import A from './components/A.vue'
import B from './components/B.vue'
</script>
<template>
<div>
<A />
<B />
</div>
</template>
<script setup>
import emitter from '../bus/EventBus'
import { ref, onUnmounted } from 'vue';
let sayB = ref(null)
emitter.on('eventA', data => {
console.log('Received eventA:', data);
sayB.value = data;
});
function handleClick() {
emitter.emit('eventB', 'Hello from ComponentA');
};
onUnmounted(()=>{
emitter.off('eventB')
})
</script>
<template>
<div>
<h1>B组件对A组件说:{{ sayB }}</h1>
<button @click="handleClick">发送给B</button>
</div>
</template>
<script setup>
import emitter from '../bus/EventBus'
import { ref, onUnmounted } from 'vue';
let sayA = ref(null);
emitter.on('eventB', data => {
console.log('Received eventB:', data);
sayA.value = data;
})
function handleClick() {
emitter.emit('eventA', 'Hello from ComponentB');
}
onUnmounted(()=>{
emitter.off('eventA')
})
</script>
<template>
<div>
<h1>A组件对B组件说:{{ sayA }}</h1>
<button @click="handleClick">发送给A</button>
</div>
</template>
父传子传孙(透传)
透传 attribute
“透传 attribute”指的是父组件传递给子组件,却没有被子组件声明为 props 或 emits 的 attribute 与 v-on 事件监听器。
当一个子组件以单根元素作渲染时,透传的 attribute 会默认自动被添加到子组件的根元素上,且该子组件会在根节点上渲染其另一个单根子组件的根节点。
注意
“透传 attribute”只作用于单根子组件上,多根节点的子组件没有自动透传 attribute 行为。
inheritAttrs
如果不想要一个组件自动地透传 attribute,你可以在组件选项中设置 inheritAttrs: false。
<script setup>
defineOptions({
// 禁用透传 attribute
inheritAttrs: false
})
</script>
$attrs
透传进来的 attribute 可以在 <template>
中(<script setup>
中没有const attrs = useAttrs()
情况下)直接用 $attrs 访问到(除组件声明的 props 和 emits 之外):
<span>Fallthrough attribute: {{ $attrs }}</span>
TIP
- 像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问
- 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick
注意
- 通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现自定义透传到目标节点上
- 多根子组件必须使用 v-bind="$attrs" 来显式绑定一个目标节点,否则将会抛出一个运行时警告
useAttrs()
在 <script setup>
中使用 useAttrs()
API 来访问一个组件的所有透传 attribute:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
注意
- 透传进来的 attribute 可以在
<template>
中直接用 attrs 访问到(除组件声明的 props 和 emits 之外) - attrs 并不是响应式对象,不能通过侦听器去监听它的变化。
示例
<!--父组件-->
<script setup>
import {ref} from 'vue'
import Child from './components/child.vue';
let nav = ref('nav')
const customClick = () => {
alert(123)
}
</script>
<template>
<div>
<h2>父组件</h2>
<Child
:class="nav"
title="Jack"
style="background-color: hotpink;"
@click="customClick"
/>
</div>
</template>
<!--子组件 child.vue-->
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs();
defineProps(["title"]);
</script>
<template>
<div>
<h2>子组件</h2>
<p>div上元素:{{ attrs }}</p>
<p>非透传的属性:{{ title }}</p>
<button @click.stop>点我试试</button>
</div>
</template>
爷传孙(依赖注入)
provide()
为组件后代提供数据,需要使用到 provide() 函数:
<script setup>
import { provide } from 'vue'
provide(injectName, provideValue)
</script>
TIP
- injectName(注入名):可以是一个字符串或是一个 Symbol
- provideValue(提供值):可以是任意类型,但基础类型不具有非响应式,引用类型具有响应式
全局 app.provide()
import { createApp } from 'vue'
const app = createApp({})
app.provide(injectName, provideValue)
inject()
注入上层组件提供的数据,需使用 inject() 函数:
<script setup>
import { inject } from 'vue'
const message = inject(injectName)
</script>
默认值 inject()
const value = inject('message', defaultValue)
示例
<!--顶层组件-->
<script setup>
import {provide, ref} from 'vue';
import Comp from './Comp.vue';
provide('data-key', 'this is room data');
const count = ref(10);
provide('count-key', count);
const addCount = () => {
count.value++;
}
provide('addCount-key', addCount);
</script>
<template>
<div>
<h1>顶层组件</h1>
<Comp />
</div>
</template>
<!--底层组件-->
<script setup>
import {inject} from 'vue';
const dataKey = inject('data-key');
const countKey = inject('count-key');
const addCountKey = inject('addCount-key');
</script>
<template>
<div>
<h1>底层组件</h1>
<p>来自顶层组件中的数据为:{{dataKey}}</p>
<p>来自顶层组件的响应式数据:{{countKey}}</p>
<button @click="addCountKey">来自顶层组件的事件方法数据</button>
</div>
</template>
slot 插槽
默认插槽
<slot></slot>
默认内容
<slot>
Submit <!-- 默认内容 -->
</slot>
- 单标签闭合子组件:启用子组件插槽默认内容
- 双标签闭合子组件:若父组件提供内容,则替换子组件插槽默认内容
具名插槽
要为具名插槽传入内容,需要使用一个含 v-slot 指令的 <template>
元素,并将目标插槽的名字传给该指令,通常适用子组件中拥有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>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
<!--简写形式-->
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 隐式的默认插槽 -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
插槽传参(作用域插槽)
默认作用域插槽
<!-- 子组件-->
<div>
<slot text="hello" :count="sum"></slot>
</div>
<!--父组件-->
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<!--解构赋值-->
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
具名作用域插槽
<div>
<slot name="header" message="hello"></slot>
</div>
<MyComponent>
<template #header="slotProps">
{{ slotProps.message }}
</template>
</MyComponent>
注意
如果同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>
组合式函数
组合式函数侧重于有状态的逻辑。
定义
组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。它基于函数而不是选项对象的方式来组织和重用组件的代码。通过将相关的功能逻辑封装到单个函数中,可以更好地组织代码、提高可读性和可维护性。
组合式函数通过 setup 函数来定义,它是一个特殊的函数,在组件实例创建时调用。setup 函数中可以访问到组件实例的上下文,并可以返回一个包含组件逻辑的对象或函数。这样可以将组件的状态、计算属性、方法等逻辑封装在一个函数中,以便于重用和测试。
组合式函数可以使用 Vue 3 提供的一些响应式 API 来处理组件的状态和副作用,例如 reactive、ref、watch、computed 等。通过组合式函数,可以更灵活地组合和管理组件的功能,使代码更模块化、可组合和可测试。
命名
组合式函数约定用驼峰命名法命名,并以“use”作为开头。
ref 参数
如果编写的组合式函数会被其他开发者使用,最好在处理输入参数时兼容 ref 而不只是原始的值。unref() 工具函数会对此非常有帮助:
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
WARNING
如果组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 watch() 显式地监听此 ref,或者在 watchEffect() 中调用 unref() 来进行正确的追踪。
返回值
组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性。
如果更希望以对象属性的形式来使用组合式函数中返回的状态,可以将返回的对象用 reactive() 包装一次,这样其中的 ref 会被自动解包。
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
副作用
在 Vue 3 中,副作用(side effect)是指在组件生命周期钩子函数或响应式 API 中执行的操作,这些操作可能会对组件的状态产生影响或与外部环境进行交互,例如发送网络请求、订阅事件、操作 DOM 等。
副作用是指那些不纯粹的操作,即对组件状态之外的内容进行的操作。在 Vue 3 中,副作用应该被放在 onMounted、onUpdated、onUnmounted 这些生命周期钩子函数或 watch、watchEffect 这些响应式 API 中执行。
Vue 3 为了提高性能和优化渲染,引入了 setup 函数,它与组合式 API 结合使用。在 setup 函数中,可以通过返回一个带有副作用的函数来执行副作用操作,例如发送网络请求、订阅事件等。
副作用操作需要注意以下几点:
- 副作用操作应该在合适的生命周期钩子函数或响应式 API 中执行,确保在正确的时间进行。
- 如果副作用操作依赖于响应式数据,请使用 watch 或 watchEffect 来进行依赖追踪,以确保副作用正确地响应数据变化。
- 在组件销毁时,需要清理副作用操作,以防止内存泄漏。可以在 onUnmounted 钩子函数中进行清理操作。
响应式 API
响应式 API 是 Vue 3 提供的一组函数和工具,用于处理组件中的响应式数据。响应式数据是指当数据发生变化时,可以自动触发相关依赖的更新,以保持视图与数据的同步。
在 Vue 3 中,响应式 API 提供了以下几个常用函数和对象:
- ref:将普通数据包装成响应式对象。
- reactive:将对象转换为响应式对象。
- computed:创建一个计算属性,其值会根据依赖的响应式数据动态计算。
- watch:监视响应式数据的变化,并执行相应的回调函数。
- watchEffect:监视响应式数据的变化,并自动运行一个响应式函数。
响应式 API 可以帮助开发者更方便地管理组件中的数据和状态,同时自动处理依赖追踪和更新触发。它提供了一种声明式的方式来处理数据变化,并与模板、计算属性、副作用等其他功能结合使用,实现组件的响应式和动态行为。
Plugins 插件
app.use()
安装一个插件。
app.use(pluginName[, options])
TIP
若 app.use() 对同一个插件多次调用,该插件只会被安装一次。
编写插件
export default {
install: (app, options) => {
// 在这里编写插件代码
}
}
app.version
提供当前应用所使用的 Vue 版本号,常用于判断插件是否支持该版本号的 Vue。
export default {
install(app) {
const version = Number(app.version.split('.')[0])
if (version < 3) {
console.warn('This plugin requires Vue 3')
}
}
}
TypeScript
为组件的 props 标注类型
使用 <script setup>
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
或
<script setup lang="ts">
import type { Props } from './foo'
const props = defineProps<Props>()
</script>
Props 解构默认值
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
复杂的 prop 类型
<script setup lang="ts">
interface Book {
title: string
author: string
year: number
}
const props = defineProps<{
book: Book
}>()
</script>
为组件的 emits 标注类型
<script setup lang="ts">
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
</script>
为 ref() 标注类型
import { ref } from 'vue'
import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!
或
const year = ref<string | number>('2020')
year.value = 2020 // 成功!
TIP
如果你指定了一个泛型参数但没有给出初始值,那么最后得到的就将是一个包含 undefined 的联合类型:
// 推导得到的类型:Ref<number | undefined>
const n = ref<number>()
为 reactive() 标注类型
import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 指引' })
DANGER
不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
为 computed() 标注类型
const double = computed<number>(() => {
// 若返回值不是 number 类型则会报错
})
为事件处理函数标注类型
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
为 provide / inject 标注类型
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
provide(key, 'foo') // 若提供的是非字符串值会导致错误
const foo = inject(key) // foo 的类型:string | undefined
为模板引用标注类型
<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>
为组件模板引用标注类型
<script setup lang="ts">
import { ref } from 'vue'
import MyModal from './MyModal.vue'
const modal = ref<InstanceType<typeof MyModal> | null>(null)
const openModal = () => {
modal.value?.open()
}
</script>
TIP
如果组件的具体类型无法获得,或者你并不关心组件的具体类型,那么可以使用 ComponentPublicInstance。
import { ref } from 'vue'
import type { ComponentPublicInstance } from 'vue'
const child = ref<ComponentPublicInstance | null>(null)