前言:参考Vue 官方文档,本文档主要学习组合式 API。
一 开始
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>
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 已经更新了
}
3.2 reactive()
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 混着用,注意那些局限性就可以了。
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 表达式:
<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>
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>
可以使用