前言:参考Vue 官方文档,本文档主要学习组合式 API。
一款 JS 框架,并有两个核心功能:声明式渲染、响应性。
根据不同的需求场景,使用不同方式的 Vue,比如:
????????无需构建步骤,直接引入 vuejs。
????????在任何页面中作为 Web Components 嵌入
????????使用构建步骤,单页应用 (SPA)
????????全栈 / 服务端渲染 (SSR)
????????Jamstack / 静态站点生成 (SSG)
????????开发桌面端、移动端、WebGL,甚至是命令行终端中的界面
Vue 为什么可以称为“渐进式框架”:它是一个可以与你共同成长、适应你不同需求的框架。
单文件组件是 Vue 的标志性功能。*.vue、SFC 就是单文件组件:将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。
选项式 API(Options API):包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。
组合式 API(Composition API):使用导入的 API 函数来描述组件逻辑。通常会与 <script setup> 搭配使用。
综上:两种 API 是同一个底层系统构建的。选项式 API 是在组合式 API 的基础上实现的!
// 安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
npm create vue@latest
可以用于增强静态的 HTML 或与后端框架集成。但将无法使用单文件组件 (SFC) 语法:
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const message = ref("Hello vue!");
return {
message,
};
},
}).mount("#app");
</script>
?通过 CDN 以及原生 ES 模块使用 Vue:
<div id="app">{{ message }}</div>
<script type="module">
import {
createApp,
ref,
} from "https://unpkg.com/vue@3/dist/vue.esm-browser.js";
createApp({
setup() {
const message = ref("Hello Vue!");
return {
message,
};
},
}).mount("#app");
</script>
使用导入映射表 (Import Maps) 来告诉浏览器如何定位到导入的 vue:
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<div id="app">{{ message }}</div>
<script type="module">
import { createApp, ref } from "vue";
createApp({
setup() {
const message = ref("Hello Vue!");
return {
message,
};
},
}).mount("#app");
</script>
分割代码成单独的 JS 文件,以便管理。
<!-- index.html -->
<div id="app"></div>
<script type="module">
import { createApp } from "vue";
import MyComponent from "./my-component.js";
createApp(MyComponent).mount("#app");
</script>
// my-component.js
import { ref } from "vue";
export default {
setup() {
const count = ref(0);
return { count };
},
template: `<div>count is {{ count }}</div>`,
};
注意:直接点击 index.html,会抛错误,因为 ES 模块不能通过 file:// 协议工作。只能通过 http:// 协议工作。需要启动一个本地的 HTTP 服务器,通过命令行在 HTML 文件所在文件夹下运行 npx serve。
通过 createApp 函数创建 Vue 应用实例:
import { createApp } from "vue";
const app = createApp({
/* 根组件选项 */
});
createApp 需要传入一个根组件,其他组件将作为其子组件:
import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";
const app = createApp(App);
调用 .mount() 方法,传入一个 DOM 元素或是 CSS 选择器。它的返回值是根组件实例而非应用实例:
<div id="app"></div>
import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";
const app = createApp(App);
app.mount("#app");
注意:确保在挂载应用实例之前完成所有应用配置!
import { createApp } from "vue";
// 从一个单文件组件中导入根组件
import App from "./App.vue";
const app = createApp(App);
// 应用实例的 .config 对象可以进行一些配置,例如配置错误处理器:用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
};
// 全局挂载组件
app.component("TodoDeleteButton", TodoDeleteButton);
// 全局属性的对象。
app.config.globalProperties.msg = "hello";
app.mount("#app");
每个应用都拥有自己的用于配置和全局资源的作用域:
const app1 = createApp({
/* ... */
});
app1.mount("#container-1");
const app2 = createApp({
/* ... */
});
app2.mount("#container-2");
最基本的数据绑定是文本插值,使用“Mustache”语法 (即双大括号):
<span>Message: {{ msg }}</span>
双大括号会将数据解释为纯文本,若想插入 HTML,需要使用 v-html 指令。
安全警告:动态渲染 HTML 是很危险的,容易造成 XSS 漏洞。仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
绑定 attribute,使用 v-bind 指令:
<div v-bind:id="dynamicId"></div>
<!-- 简写 -->
<div :id="dynamicId"></div>
绑定多个值,通过不带参数的 v-bind。
const objectOfAttrs = {
id: "container",
class: "wrapper",
};
<div v-bind="objectOfAttrs"></div>
数据绑定都支持完整的 JS 表达式,也就是一段能够被求值的 JS 代码。一个简单的判断方法是是否可以合法地写在 return 后面:
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
可以在绑定的表达式中使用一个组件暴露的方法:
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
</time>
指 v- 前缀的特殊 attribute。它的值为 JS 表达式(v-for、v-on、v-slot 除外),指令值变化时响应式的更新 DOM,比如 v-if:
<p v-if="seen">Now you see me</p>
带参数的指令,用':'隔开:
<a v-bind:href="url"> ... </a>
<!-- 简写 -->
<a :href="url"> ... </a>
指令的参数,也可以动态绑定,用 '[ ]' 包裹:
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
带修饰符的指令,用 '.' 隔开:
<!-- 触发的事件调用 event.preventDefault() -->
<form @submit.prevent="onSubmit">...</form>
官方推荐使用 ref() 函数来声明响应式状态:
import { ref } from "vue";
const count = ref(0);
ref() 接收参数,并返回一个带有 .value 属性的 ref 对象:
const count = ref(0);
console.log(count); // { value: 0 }
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
在模板中使用 ref 变量,不需要添加 .value。ref 会自动解包。也可以直接在事件监听器中改变一个 ref:
<button @click="count++">{{ count }}</button>
通过单文件组件(SFC),使用 <script setup> 来大幅度地简化代码:
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
为什么使用 ref,而不是普通的变量。这是因为 Vue 需要通过.value 属性来实现状态响应性。基础原理是在 getter 中追踪,在 setter 中触发:
// 伪代码,不是真正的实现
const myRef = {
_value: 0,
get value() {
track();
return this._value;
},
set value(newValue) {
this._value = newValue;
trigger();
},
};
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
import { nextTick } from "vue";
async function increment() {
count.value++;
await nextTick();
// 现在 DOM 已经更新了
}
reactive(),参数只能是对象类型,返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
const raw = {};
const proxy = reactive(raw);
// 代理对象和原始对象不是全等的
console.log(proxy === raw); // false
reactive() 局限性包括:只能用于对象类型(对象,数组,Map,Set)、不能替换整个对象、对结构操作不友好:
let state = reactive({ count: 0 });
// 上面的 ({ count: 0 }) 引用将不再被追踪
// (响应性连接已丢失!)
state = reactive({ count: 1 });
// 对解构不友好
const state = reactive({ count: 0 });
// 当解构时,count 已经与 state.count 断开连接
let { count } = state;
// 不会影响原始的 state
count++;
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count);
reactive() API 有一些局限性,官方建议使用 ref() 作为声明响应式状态的主要 API。博主个人还是喜欢 ref,reactive 混着用,注意那些局限性就可以了。
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包:
const count = ref(0);
const state = reactive({
count,
});
console.log(state.count); // 0
state.count = 1;
console.log(count.value); // 1
computed() 方法期望接收一个 getter 函数,返回为一个计算属性 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(() => {
return author.books.length > 0 ? "Yes" : "No";
});
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
计算属性值会基于其响应式依赖被缓存。方法调用则总会在重新渲染时再次执行。
计算属性默认是只读的。但可以通过设置 get 和 set 函数变成可读可写:
<script setup>
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(" ");
},
});
</script>
使用计算属性,不要在里面做异步请求和修改 DOM。并且尽量保持只读。
通过对象来动态切换 class:
<div :class="{ active: isActive }"></div>
可以直接绑定一个对象:
const classObject = reactive({
active: true,
"text-danger": false,
});
<div :class="classObject"></div>
通过数组渲染多个 class:
const activeClass = ref("active");
const errorClass = ref("text-danger");
<div :class="[activeClass, errorClass]"></div>
数组中也可以使用 JS 表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<!-- 等于 -->
<div :class="[{ activeClass: isActive }, errorClass]"></div>
如果组件有多个根元素,透传的 class 需要通过组件的 $attrs 属性来实现指定:
<MyComponent class="baz" />
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<p class="baz">Hi!</p>
<span>This is a child component</span>
值为对象,对应的是 style 属性:
const activeColor = ref("red");
const fontSize = ref(30);
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
直接绑定一个样式对象使模板更加简洁:
const styleObject = reactive({
color: "red",
fontSize: "13px",
});
<div :style="styleObject"></div>
还可以绑定一个包含多个样式对象的数组:
<div :style="[baseStyles, overridingStyles]"></div>
v-if 指令用于条件性地渲染内容。当值为真时才被渲染:
<h1 v-if="awesome">Vue is awesome!</h1>
v-else 为 v-if 添加一个“else 区块”。并且必须跟在一个 v-if 或者 v-else-if 元素后面:
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
v-else-if 提供的是相应于 v-if 的“else if 区块”。可以连续多次使用。并且必须跟在一个 v-if 或者 v-else-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> 包裹想要一起切换的元素块,渲染的结果并不会包含这个 <template> 元素。v-else、v-else-if 同理:
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-show 仅切换了元素上 display 的 CSS 属性。且不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。
v-if 切换的组件都会被销毁与重建。但是如果初始条件为 false,则不会做任何事,有更高的切换开销。
v-show 切换的组件只有 display 属性被修改,但初始化都会渲染。有更高的渲染开销。
综上:如果切换频繁用 v-show,反之用 v-if。
v-if 和 v-for 不推荐同时使用,因为这样二者的优先级不明显。如果二者同时存在一个元素上,v-if 优先执行。
v-for 指令基于一个数组来渲染一个列表:
const parentMessage = ref("Parent");
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
可以使用 of 替代 in:
<div v-for="item of items"></div>
v-for 可以遍历对象属性:
const myObject = reactive({
title: "How to do lists in Vue",
author: "Jane Doe",
publishedAt: "2016-04-10",
});
<!-- 第二个参数表示属性名,第三个参数表示位置索引 -->
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
v-for 可以接受一个整数。从 1~n 开始遍历:
<span v-for="n in 10">{{ n }}</span>
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
在同一节点上,v-if 比 v-for 优先级更高。意味着 v-if 的条件无法访问到 v-for 中的变量:
<!-- 会抛出一个错误,因为v-if访问不到属性 todo -->
<li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li>
<!-- 解决方法:包裹一层template -->
<template v-for="todo in todos">
<li v-if="!todo.isComplete">{{ todo.name }}</li>
</template>
Vue 默认的 “就地更新” 策略是高效的,但当数据源的顺序改变时,Vue 不会随之移动 DOM 顺序,而是就地更新每个元素。解决这一问题需要给每个元素添加一个 Key 值,官方推荐使用 v-for 都添加 key 值:
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
Vue 能侦听响应式数组的变化。改变原数组的方法:push(),pop(),shift(),unshift(),splice(),sort(),reverse()。不改变元素组:filter(),concat(),slice()。
使用 computed,在不修改数据源的前提下,展示过滤或排序后的数据:
const numbers = ref([1, 2, 3, 4, 5]);
const evenNumbers = computed(() => {
return numbers.value.filter((n) => n % 2 === 0);
});
<li v-for="n in evenNumbers">{{ n }}</li>
使用 v-on 指令 (简写 @) 来监听 DOM 事件
const count = ref(0);
<button @click="count++">Add 1</button>
const name = ref("Vue.js");
function greet(event) {
alert(`Hello ${name.value}!`);
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName);
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
function say(message) {
alert(message);
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>
通过 $event 变量访问原生 DOM,或使用内联箭头函数中的 event 形参:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">Submit</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault();
}
alert(message);
}
修饰符是用 . 表示的指令后缀,包含:.stop,.prevent,.self,.capture,.once,.passive:
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
.capture、.once 和 .passive 修饰符与原生 addEventListener 事件相对应:
<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>
<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<!-- .passive 修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。 -->
<div @scroll.passive="onScroll">...</div>
.enter,.tab,.delete (捕获“Delete”和“Backspace”两个按键),.esc,.space,.up,.down,.left,.right
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
v-model 数据双向绑定
<input v-model="text" />
<!-- 等价于 -->
<input :value="text" @input="event => text = event.target.value" />
<!-- 文本 -->
<input v-model="message" placeholder="edit me" />
<!-- 多行文本 -->
<textarea v-model="message" placeholder="add multiple lines"></textarea>
<!-- 复选框 -->
<input type="checkbox" id="checkbox" v-model="checked" />
<!-- 单选按钮 -->
<input type="radio" id="one" value="One" v-model="picked" />
<!-- 选择器 -->
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- 复选框 -->
<!-- true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。 -->
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
<!-- 单选按钮 -->
<!-- pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。 -->
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />
<!-- 选择器选项 -->
<!-- v-model 同样也支持非字符串类型的值绑定!
在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }。 -->
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
<!-- .lazy -->
<!-- 在 "change" 事件后同步更新而不是 "input" -->
<input v-model.lazy="msg" />
<!-- .number -->
<!-- 让用户输入自动转换为数字 -->
<input v-model.number="age" />
<!-- .trim -->
<!-- 默认自动去除用户输入内容中两端的空格 -->
<input v-model.trim="msg" />
Vue 组件实例的创建到销毁,一些列的生命周期钩子函数,可以让我们特定阶段运行自己的代码。最常用的是 onMounted、onUpdated 和 onUnmounted:
<script setup>
import { onMounted, onUpdated, onUnmounted } from "vue";
// 初始渲染、创建 DOM 节点后
onMounted(() => {
console.log(`the component is now mounted.`);
});
// 组件更新 DOM 树之后。
onUpdated(() => {
// 文本内容应该与当前的 `count.value` 一致
console.log(document.getElementById("count").textContent);
});
// 组件实例被卸载之后。
let intervalId;
onMounted(() => {
intervalId = setInterval(() => {
// ...
});
});
onUnmounted(() => clearInterval(intervalId));
</script>
<script setup>
import { ref, watch } from "vue";
const question = ref("");
const answer = ref("Questions usually contain a question mark. ;-)");
const loading = ref(false);
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes("?")) {
loading.value = true;
answer.value = "Thinking...";
try {
const res = await fetch("https://yesno.wtf/api");
answer.value = (await res.json()).answer;
} catch (error) {
answer.value = "Error! Could not reach the API. " + error;
} finally {
loading.value = false;
}
}
});
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
const x = ref(0);
const y = ref(0);
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`);
});
// 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 obj = reactive({ count: 0 });
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`);
});
// 解决:提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`);
}
);
给 watch() 传入一个响应式对象,会监听对象的所有属性:
const obj = reactive({ count: 0 });
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
});
obj.count++;
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。可以通过设置 immediate:true,立即执行一遍回调:
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true }
);
侦听器的回调与源完全相同的响应式状态是很常见的。可以用 watchEffect 函数 来简化:
const todoId = ref(1);
const data = ref(null);
watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
);
data.value = await response.json();
},
{ immediate: true }
);
// 简化
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
);
data.value = await response.json();
});
修改了监听的响应状态后,默认先触发监听函数,后更新组件。意味着在监听函数中访问的 DOM 是更新前的状态,如果要访问更新后的 DOM,可以通过 fulsh:'post'配置:
watch(source, callback, {
flush: "post",
});
watchEffect(callback, {
flush: "post",
});
// 或者
import { watchPostEffect } from "vue";
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
});
使用 ref attribute 访问 DOM 元素:
<input ref="input" />
声明一个同名的 ref:
<script setup>
import { ref, onMounted } from "vue";
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null);
onMounted(() => {
input.value.focus();
});
</script>
<template>
<input ref="input" />
</template>
侦听模板引用 ref 的变化,需要考虑值为 null 的情况:
watchEffect(() => {
if (input.value) {
input.value.focus();
} else {
// 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
}
});
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 attribute 还可以绑定为一个函数,每次组件更新时都被调用。收到的第一个参数是元素引用:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" />
组件允许我们将 UI 划分为独立的、可重用的部分,并且对每个部分进行单独的思考。
.vue 文件 (简称 SFC):
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
官方推荐使用 PascalCase 标签名用于和原生 HTML 标签区分:
<script setup>
import ButtonCounter from "./ButtonCounter.vue";
</script>
<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>
通过 defineProps 宏,声明可以向组件传递的状态:
<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
</script>
<template>
<h4>{{ title }}</h4>
</template>
通过 defineEmits 宏,声明需要抛出的事件。子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:
<!-- BlogPost.vue -->
<script setup>
defineProps(["title"]);
const emit = defineEmits(["enlarge-text"]);
const handleClick = () => {
emit("enlarge-text");
};
</script>
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="handleClick">Enlarge text</button>
</div>
</template>
通过 <slot> 元素来实现:
<!-- AlertBox.vue -->
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>
<style scoped>
.alert-box {
/* ... */
}
</style>
<AlertBox> Something bad happened. </AlertBox>
通过 <component :is="..."> 来在多个组件间作切换,可以使用 <KeepAlive> 保持被切换的组件“存活”的状态:
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>
使用 .component() 方法,让组件在全局可用:
import { createApp } from "vue";
const app = createApp({});
app.component(
// 注册的名字
"MyComponent",
// 组件的实现
{
/* ... */
}
);
.component() 可以链式调用:
app
.component("ComponentA", ComponentA)
.component("ComponentB", ComponentB)
.component("ComponentC", ComponentC);
注意:全局注册存在以下几个问题:1. 全局注册组件后,即使没有被实际使用,仍会被打包在 JS 文件中。2. 全局注册使依赖关系变得不那么明确。不容易定位子组件的实现。
导入后可以直接使用:
<script setup>
import ComponentA from "./ComponentA.vue";
</script>
<template>
<ComponentA />
</template>
官方推荐使用 PascalCase 作为组件名的注册格式。
使用 defineProps() 宏来声明:
<script setup>
// defineProps传入字符串数组
const props = defineProps(["foo"]);
// 或 传入对象:可进行类型检测。
const props = defineProps({
title: String,
likes: Number,
});
console.log(props.foo);
</script>
如果 prop 的名字很长,官方建议使用 camelCase 形式:
defineProps({
greetingMessage: String,
});
传递 props 时,官方推荐以 kebab-case 形式:
<MyComponent greeting-message="hello" />
传递不同类型的值:
<!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :likes="42" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :likes="post.likes" />
<!-- 仅写上 prop 但不传值,会隐式转换为 `true` -->
<BlogPost is-published />
<!-- 虽然 `false` 是静态的值,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :is-published="false" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :is-published="post.isPublished" />
<!-- 虽然这个数组是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<BlogPost :comment-ids="[234, 266, 273]" />
<!-- 根据一个变量的值动态传入 -->
<BlogPost :comment-ids="post.commentIds" />
<!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind -->
<!-- 因为这是一个 JS 表达式而不是一个字符串 -->
<!-- <BlogPost
:author="{
name: 'Veronica',
company: 'Veridian Dynamics',
}"
/> -->
一个对象绑定多个 prop:
const post = {
id: 1,
title: "My Journey with Vue",
};
<BlogPost v-bind="post" />
<!-- 等价于: -->
<BlogPost :id="post.id" :title="post.title" />
props 都遵循单向绑定原则,不允许在子组件中去更改一个 prop。但有两个场景可能想要修改 prop:1.prop 被用于传入初始值,之后想将其作为一个响应式属性。2. 需要对传入的 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";
},
},
});
自定义类或构造函数去验证,校验是否 Person 类的实例:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
defineProps({
author: Person,
});
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
模板表达式可以直接使用 $emit 触发自定义事件:
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
父组件通过@来监听事件:
<MyComponent @some-event="callback" />
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
<MyButton @increase-by="(n) => (count += n)" />
通过 defineEmits() 宏来声明要触发的事件:
<script setup>
const emit = defineEmits(["inFocus", "submit"]);
function buttonClick() {
emit("submit");
}
</script>
校验事件参数是否符合要求,返回布尔值表明事件是否符合:
<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>
v-model 在 input 上的用法:
<input v-model="searchText" />
<!-- 展开 -->
<input :value="searchText" @input="searchText = $event.target.value" />
v-model 在组件上的用法,通过 modelValue,update:modelValue 设置:
<!-- CustomInput.vue -->
<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<CustomInput v-model="searchText" />
<!-- 展开 -->
<CustomInput
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<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>
<MyComponent v-model:title="bookTitle" />
<!-- MyComponent.vue -->
<script setup>
defineProps(["title"]);
defineEmits(["update:title"]);
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
<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>
<MyComponent v-model.capitalize="myText" />
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);
function emitValue(e) {
let value = e.target.value;
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit("update:modelValue", value);
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
v-model 参数和修饰符一起绑定:
<MyComponent v-model:title.capitalize="myText"></MyComponent>
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
class、style、id 传递给组件,透传给组件的元素。如果只有一个根节点,默认透传给根节点上,如果有多个根节点,通过 v-bind='$attrs'来确定透传给哪个根节点。
应用场景:组件只有一个根节点,但透传的 attribute 需要传到其他元素上。通过设置 inheritAttrs 选项为 false,v-bind='$attrs',来完全控制透传:
<script setup>
defineOptions({
inheritAttrs: false,
});
// ...setup 逻辑
</script>
<template>
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
</template>
使用 useAttrs() API 来访问一个组件的所有透传 attribute:
<script setup>
import { useAttrs } from "vue";
const attrs = useAttrs();
</script>
通过 <slot> 确定插槽插入的位置:
<!-- FancyButton.vue -->
<button class="fancy-btn">
<!-- 插槽出口 -->
<slot></slot>
</button>
<FancyButton>
<!-- 插槽内容 -->
Click me!
</FancyButton>
插槽内容可以访问父组件的状态,无法访问子组件的状态:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
外部没有传递内容,展示默认内容:
<button type="submit">
<slot>
<!-- 默认内容 -->
Submit
</slot>
</button>
通过 <slot name=''>来设定,name 默认值为 default:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot name="default"></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
通过 v-slot 有对应的简写 # 来使用,因此 <template v-slot:header> 可以简写为 <template #header>:
<BaseLayout>
<template #header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
<BaseLayout>
<template v-slot:[dynamicSlotName]> ... </template>
<!-- 缩写为 -->
<template #[dynamicSlotName]> ... </template>
</BaseLayout>
插槽内容同时访问父组件和子组件的状态:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
向具名插槽中传入 props:
<!-- MyComponent.vue -->
<slot name="header" message="hello"></slot>
具名插槽访问子组件的状态:
<MyComponent>
<template #header="headerProps"> {{ headerProps }} </template>
<template #default="defaultProps"> {{ defaultProps }} </template>
<template #footer="footerProps"> {{ footerProps }} </template>
</MyComponent>
解决多层级嵌套的组件,共享祖先状态。使用 provide 和 inject 可以帮助我们解决。
<script setup>
import { provide } from "vue";
provide(/* 注入名 */ "message", /* 值 */ "hello!");
</script>
import { createApp } from "vue";
const app = createApp({});
app.provide(/* 注入名 */ "message", /* 值 */ "hello!");
<script setup>
import { inject } from "vue";
const message = inject("message", "这是默认值");
</script>
官方建议将共享状态和修改共享状态的方法都 Provide 出去:
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from "vue";
const location = ref("North Pole");
function updateLocation() {
location.value = "South Pole";
}
provide("location", {
location,
updateLocation,
});
</script>
使用 readonly(),注入方就只读不可修改:
<script setup>
import { ref, provide, readonly } from "vue";
const count = ref(0);
provide("read-only-count", readonly(count));
</script>
仅在需要页面渲染时加载组件。实现延迟加载。全局注册:
app.component(
"MyComponent",
defineAsyncComponent(() => import("./components/MyComponent.vue"))
);
局部注册:
<script setup>
import { defineAsyncComponent } from "vue";
const AdminPage = defineAsyncComponent(() =>
import("./components/AdminPageComponent.vue")
);
</script>
<template>
<AdminPage />
</template>
异步操作不可避免地会涉及到加载和错误状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import("./Foo.vue"),
// 加载中使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000,
});
“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
使用组合式 API 实现鼠标跟踪功能:
// 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>
一个组合式函数可以调用一个或多个其他的组合式函数。这样就可以用多个较小且逻辑独立的单元来组合形成复杂的逻辑。举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:
// event.js
import { onMounted, onUnmounted } from "vue";
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback));
onUnmounted(() => target.removeEventListener(event, callback));
}
// mouse.js
import { ref } from "vue";
import { useEventListener } from "./event";
export function useMouse() {
const x = ref(0);
const y = ref(0);
useEventListener(window, "mousemove", (event) => {
x.value = event.pageX;
y.value = event.pageY;
});
return { x, y };
}
在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
组合式函数约定用驼峰命名法命名,并以“use”作为开头:
import { toValue } from "vue";
function useFeature(maybeRefOrGetter) {
// Todo
}
组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数
和 Mixin 的对比,mixins 有三个主要的短板:不清晰的数据来源、命名空间冲突、隐式的跨 mixin 交流
和无渲染组件的对比:组合式函数不会产生额外的组件实例开销。
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令:
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus(),
};
</script>
<template>
<input v-focus />
</template>
全局注册自定义指令:
const app = createApp({});
// 使 v-focus 在所有组件中都可用
app.directive("focus", {
/* ... */
});
指令钩子函数都是可选的:
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"></div>
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
一般情况指令仅需要 mounted 和 updated 实现相同行为,可以直接用一个函数定义指令:
app.directive("color", (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value;
});
指令值可以接受一个 JS 对象字面量:
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive("demo", (el, binding) => {
console.log(binding.value.color); // => "white"
console.log(binding.value.text); // => "hello!"
});
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:
import { createApp } from "vue";
const app = createApp({});
app.use(myPlugin, {
/* 可选的选项 */
});
构建插件:拥有 install() 方法的对象,接受两个参数一个应用实例和一个传递给 app.use() 的额外参数:
const myPlugin = {
install(app, options) {
// 配置此应用
},
};
编写一个翻译函数,接收一个以 . 作为分隔符的 key 字符串。可以在全局使用,通过 app.config.globalProperties 实现:
// plugins/i18n.js
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split(".").reduce((o, i) => {
if (o) return o[i];
}, options);
};
},
};
翻译字典对象应当作为 app.use() 的额外参数被传入:
import i18nPlugin from "./plugins/i18n";
app.use(i18nPlugin, {
greetings: {
hello: "Bonjour!",
},
});
使用插件:
<h1>{{ $translate('greetings.hello') }}</h1>
会在一个元素或组件进入和离开 DOM 时应用动画,如果内容是一个组件,这个组件必须仅有一个根元素。
可以直接使用,无需注册。动画过渡触发条件:v-if、v-show、<component>切换组件、改变特殊的 key 属性:
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
/* 进入,离开的过渡效果 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
/* 进入前,离开后的DOM状态 */
.v-enter-from,
.v-leave-to {
opacity: 0;
}
有 6 个应用于进入和离开过渡效果的 CSS class:
v-enter-from:进入动画的起始状态。
v-enter-active:进入动画的生效状态。
v-enter-to:进入动画的结束状态。
v-leave-from:离开动画的起始状态。
v-leave-active:离开动画的生效状态。
v-leave-to:离开动画的结束状态。
使用 name prop 自定义过渡效果名:
<Transition name="fade">
...
</Transition>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
CSS 的 transition,显示样式是 DOM 的 css 的样式,只需定义进入前,离开后的 css 即可:
<Transition name="slide-fade">
<p v-if="show">hello</p>
</Transition>
/*
进入和离开动画可以使用不同
持续时间和速度曲线。
*/
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
CSS 的 animation:
<Transition name="bounce">
<p v-if="show" style="text-align: center;">
Hello here is some bouncy text!
</p>
</Transition>
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}
可自定义过渡 class,在你想集成第三方 CSS 动画库时非常有用,比如 Animate.css:
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"
>
<p v-if="show">hello</p>
</Transition>
在深层级的元素上触发过渡效果:
<Transition name="nested">
<div v-if="show" class="outer">
<div class="inner">
Hello
</div>
</div>
</Transition>
/* 应用于嵌套元素的规则 */
.nested-enter-active .inner,
.nested-leave-active .inner {
transition: all 0.3s ease-in-out;
}
.nested-enter-from .inner,
.nested-leave-to .inner {
transform: translateX(30px);
opacity: 0;
}
/* 延迟嵌套元素的进入以获得交错效果 */
.nested-enter-active .inner {
transition-delay: 0.25s;
}
/* ... 省略了其他必要的 CSS */
等待所有内部元素的过渡完成,传入 duration prop 来显式指定过渡的持续时间 (以毫秒为单位):
<Transition :duration="550">...</Transition>
<Transition :duration="{ enter: 500, leave: 800 }">...</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>
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done();
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done();
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
如果仅有 JS 控制过渡,最好手动加上:css='false',防止 CSS 规则意外干扰过渡:
<Transition ... :css="false">
...
</Transition>
将 <Transition> 封装成组件:
<!-- MyTransition.vue -->
<script>
// JS 钩子逻辑...
</script>
<template>
<!-- 包装内置的 Transition 组件 -->
<Transition name="my-transition" @enter="onEnter" @leave="onLeave">
<slot></slot>
<!-- 向内传递插槽内容 -->
</Transition>
</template>
<style>
/*
必要的 CSS...
注意:避免在这里使用 <style scoped>
因为那不会应用到插槽内容上
*/
</style>
<MyTransition>
<div v-if="show">Hello</div>
</MyTransition>