一 开始
1. 简介
1.1 什么是 Vue?
一款 JS 框架,并有两个核心功能:声明式渲染、响应性。
1.2 渐进式框架
根据不同的需求场景,使用不同方式的 Vue,比如:
无需构建步骤,直接引入 vuejs。
在任何页面中作为 Web Components 嵌入
使用构建步骤,单页应用 (SPA)
全栈 / 服务端渲染 (SSR)
Jamstack / 静态站点生成 (SSG)
开发桌面端、移动端、WebGL,甚至是命令行终端中的界面
Vue 为什么可以称为“渐进式框架”:它是一个可以与你共同成长、适应你不同需求的框架。
1.3 单文件组件
单文件组件是 Vue 的标志性功能。*.vue、SFC 就是单文件组件:将一个组件的逻辑 (JS),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。
1.4 API 风格
选项式 API(Options API):包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。
组合式 API(Composition API):使用导入的 API 函数来描述组件逻辑。通常会与 搭配使用。
综上:两种 API 是同一个底层系统构建的。选项式 API 是在组合式 API 的基础上实现的!
2. 快速上手
2.1 创建一个 Vue 应用
// 安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。 npm create vue@latest
2.2 通过 CDN 使用 Vue
可以用于增强静态的 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。
二 基础
1. 创建一个 Vue 应用
1.1 应用实例
通过 createApp 函数创建 Vue 应用实例:
import { createApp } from "vue"; const app = createApp({ /* 根组件选项 */ });
1.2 根组件
createApp 需要传入一个根组件,其他组件将作为其子组件:
import { createApp } from "vue"; // 从一个单文件组件中导入根组件 import App from "./App.vue"; const app = createApp(App);
1.3 挂载应用
调用 .mount() 方法,传入一个 DOM 元素或是 CSS 选择器。它的返回值是根组件实例而非应用实例:
<div id="app"></div>
import { createApp } from "vue"; // 从一个单文件组件中导入根组件 import App from "./App.vue"; const app = createApp(App); app.mount("#app");
1.4 应用配置
注意:确保在挂载应用实例之前完成所有应用配置!
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");
1.5 多个应用实例
每个应用都拥有自己的用于配置和全局资源的作用域:
const app1 = createApp({ /* ... */ }); app1.mount("#container-1"); const app2 = createApp({ /* ... */ }); app2.mount("#container-2");
2. 模板语法
2.1 文本插值
最基本的数据绑定是文本插值,使用“Mustache”语法 (即双大括号):
<span>Message: {{ msg }}</span>
2.2 原始 HTML
双大括号会将数据解释为纯文本,若想插入 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>
2.3 Attribute 绑定
绑定 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>
2.4 使用 JS 表达式
数据绑定都支持完整的 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>
2.5 指令 Directives
指 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>
3. 响应式基础
3.1 声明响应式状态
官方推荐使用 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:
1. import { nextTick } from "vue"; 2. 3. async function increment() { 4. count.value++; 5. await nextTick(); 6. // 现在 DOM 已经更新了 7. }
3.2 reactive()
reactive(),参数只能是对象类型,返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
import { nextTick } from "vue"; async function increment() { count.value++; await nextTick(); // 现在 DOM 已经更新了 }
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 混着用,注意那些局限性就可以了。
3.3 额外的 ref 解包细节
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包:
const count = ref(0); const state = reactive({ count, }); console.log(state.count); // 0 state.count = 1; console.log(count.value); // 1
4. 计算属性
4.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>
4.2 计算属性缓存 vs 方法
计算属性值会基于其响应式依赖被缓存。方法调用则总会在重新渲染时再次执行。
4.3 可写计算属性
计算属性默认是只读的。但可以通过设置 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>
4.4 最佳实践
使用计算属性,不要在里面做异步请求和修改 DOM。并且尽量保持只读。
5. 类与样式绑定
5.1 绑定 HTML class
通过对象来动态切换 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 表达式:
1. <div :class="[isActive ? activeClass : '', errorClass]"></div> 2. <!-- 等于 --> 3. <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>
5.2 绑定内联样式
值为对象,对应的是 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>
6. 条件渲染
6.1 v-if、v-else、v-else-if
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>
6.2 v-show
v-show 仅切换了元素上 display 的 CSS 属性。且不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。
6.3 v-if vs v-show
v-if 切换的组件都会被销毁与重建。但是如果初始条件为 false,则不会做任何事,有更高的切换开销。
v-show 切换的组件只有 display 属性被修改,但初始化都会渲染。有更高的渲染开销。
综上:如果切换频繁用 v-show,反之用 v-if。
6.4 v-if 和 v-for
v-if 和 v-for 不推荐同时使用,因为这样二者的优先级不明显。如果二者同时存在一个元素上,v-if 优先执行。
7. 列表渲染
7.1 v-for
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>
7.2 v-for 与对象
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>
7.3 在 v-for 里使用范围值
v-for 可以接受一个整数。从 1~n 开始遍历:
<span v-for="n in 10">{{ n }}</span>
7.4 <template> 上的 v-for
<ul> <template v-for="item in items"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul>
7.5 v-for 与 v-if
在同一节点上,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>
7.6 通过 key 管理状态
Vue 默认的 “就地更新” 策略是高效的,但当数据源的顺序改变时,Vue 不会随之移动 DOM 顺序,而是就地更新每个元素。解决这一问题需要给每个元素添加一个 Key 值,官方推荐使用 v-for 都添加 key 值:
<div v-for="item in items" :key="item.id"> <!-- 内容 --> </div>
7.7 数组变化侦测
Vue 能侦听响应式数组的变化。改变原数组的方法:push(),pop(),shift(),unshift(),splice(),sort(),reverse()。不改变元素组:filter(),concat(),slice()。
7.8 展示过滤或排序后的结果
使用 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>
8. 事件处理
8.1 监听事件
使用 v-on 指令 (简写 @) 来监听 DOM 事件
8.2 内联事件处理器
const count = ref(0);
<button @click="count++">Add 1</button>
8.3 方法事件处理器
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>
8.4 事件传参
function say(message) { alert(message); }
<button @click="say('hello')">Say hello</button> <button @click="say('bye')">Say bye</button>
8.5 在方法中访问原生 DOM
通过 $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); }
8.6 事件修饰符
修饰符是用 . 表示的指令后缀,包含:.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>
8.7 按键修饰符
.enter,.tab,.delete (捕获“Delete”和“Backspace”两个按键),.esc,.space,.up,.down,.left,.right
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` --> <input @keyup.enter="submit" />
9. 表单输入绑定
v-model 数据双向绑定
<input v-model="text" /> <!-- 等价于 --> <input :value="text" @input="event => text = event.target.value" />
9.1 基本用法
<!-- 文本 --> <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>
9.2 值绑定
<!-- 复选框 --> <!-- 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>
9.3 修饰符
<!-- .lazy --> <!-- 在 "change" 事件后同步更新而不是 "input" --> <input v-model.lazy="msg" /> <!-- .number --> <!-- 让用户输入自动转换为数字 --> <input v-model.number="age" /> <!-- .trim --> <!-- 默认自动去除用户输入内容中两端的空格 --> <input v-model.trim="msg" />
10. 生命周期
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>
11. 侦听器
11.1 基本示例
<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}`); } );
11.2 深层侦听器
给 watch() 传入一个响应式对象,会监听对象的所有属性:
const obj = reactive({ count: 0 }); watch(obj, (newValue, oldValue) => { // 在嵌套的属性变更时触发 // 注意:`newValue` 此处和 `oldValue` 是相等的 // 因为它们是同一个对象! }); obj.count++;
11.3 即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。可以通过设置 immediate:true,立即执行一遍回调:
watch( source, (newValue, oldValue) => { // 立即执行,且当 `source` 改变时再次执行 }, { immediate: true } );
11.4 watchEffect()
侦听器的回调与源完全相同的响应式状态是很常见的。可以用 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(); });
11.5 回调的触发时机
修改了监听的响应状态后,默认先触发监听函数,后更新组件。意味着在监听函数中访问的 DOM 是更新前的状态,如果要访问更新后的 DOM,可以通过 fulsh:'post'配置:
watch(source, callback, { flush: "post", }); watchEffect(callback, { flush: "post", }); // 或者 import { watchPostEffect } from "vue"; watchPostEffect(() => { /* 在 Vue 更新后执行 */ });
12. 模板引用
使用 ref attribute 访问 DOM 元素:
<input ref="input" />
12.1 访问模板引用
声明一个同名的 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 控制) } });
12.2 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>
12.3 函数模板引用
ref attribute 还可以绑定为一个函数,每次组件更新时都被调用。收到的第一个参数是元素引用:
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" />
13. 组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且对每个部分进行单独的思考。
13.1 定义一个组件
.vue 文件 (简称 SFC):
<script setup> import { ref } from "vue"; const count = ref(0); </script> <template> <button @click="count++">You clicked me {{ count }} times.</button> </template>
13.2 使用组件
官方推荐使用 PascalCase 标签名用于和原生 HTML 标签区分:
<script setup> import ButtonCounter from "./ButtonCounter.vue"; </script> <template> <h1>Here is a child component!</h1> <ButtonCounter /> </template>
13.3 传递 props
通过 defineProps 宏,声明可以向组件传递的状态:
<!-- BlogPost.vue --> <script setup> defineProps(["title"]); </script> <template> <h4>{{ title }}</h4> </template>
13.4 监听事件
通过 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>
13.5 通过插槽来分配内容
通过 <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>
13.6 动态组件
通过 <component :is="..."> 来在多个组件间作切换,可以使用 <KeepAlive> 保持被切换的组件“存活”的状态:
<!-- currentTab 改变时组件也改变 --> <component :is="tabs[currentTab]"></component>
Vue3 官方文档速通(中)https://developer.aliyun.com/article/1511952?spm=a2c6h.13148508.setting.33.f8774f0e6djMAY