Vue3 基础 - Vue3 Essentials
- 想起来回炉一下 6 年前学的 Vue,AI 用太多了,一点不会也不行
- 基本概念,完全基于 SFC(单文件组件)和 Composition API(组合式 API)
- ref: https://cn.vuejs.org/
创建应用
开始尝试 pnpm.
1 | pnpm create vue@latest |
响应式基础
Ref
通常采用 ref 来创建响应式数据,ref
会返回一个对象,这个对象有一个 .value
属性来存储实际的值。
从 Rust 等有引用类型的语言来看,
ref是一个相当符合直觉的概念,在模版中使用时,甚至有自动deref,通过ref创建的响应式对象相当于是到处传递指针,而非原始对象,这实现了 proxy。
1 | import { ref } from "vue"; |
这创建了一个响应式的 count 变量,初始值为
0,要在 js 中访问或修改它的值,需要使用
count.value,而在模版中可以直接使用
count。
模版中会自动解一层
ref,但是如果二层ref是文本插值的最终计算值,那么其也会自动解
使用.value的原因是需要setter/getter实现 proxy
1 | <script setup> |
这里的 setup 是一个特殊的
<script>标签,里面的内容会在组件实例化时执行,并且可以直接在模版中使用。
模版采用 Mustache 语法,采用{{ }}包裹一个表达式。
Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者
JavaScript 内置的数据结构,比如 Map。 同时 Ref
会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到
1 | import { ref } from "vue"; |
也可以通过
shallowRef创建浅层响应式对象,shallowRef只会使对象的第一层属性具有响应性,嵌套对象或数组的变化不会被检测到。
Reactive
与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性
1 | import { reactive } from "vue"; |
reactive
会返回一个响应式的对象,直接访问和修改对象的属性即可触发响应式更新。在上面这个例子中,直接可以使用
state.count 来访问和修改 count 的值,而不需要使用 .value。
需要注意的是 ,
reactive只能用于对象类型的值,事实上在 Ref 中遇到对象类型会自动用reactive包装,但它不支持基本类型,这是因为基本类型的修改本身无法被捕获,但是对象上的操作可以被捕获,同时如果将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接
一个 ref
会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
1 | const count = ref(0); |
只有当嵌套在一个深层响应式对象内时,才会发生 ref
解包。当其作为浅层响应式对象的属性被访问时不会解包。
模板语法
文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
1 | <span>Message: {{ msg }}</span> |
原始 HTML
采用 v-html 指令来输出原始
HTML,注意不要在不受信任的内容上使用 v-html,以防止 XSS
攻击。
1 | <p>Using text interpolation: {{ rawHtml }}</p> |
Attribute 绑定
1 | <div v-bind:id="dynamicId"></div> |
通过 v-bind 指令来绑定 HTML 属性到一个表达式,上面这个例子中将 id
属性绑定到一个名为 dynamicId 的变量上。v-bind
可以绑定任何有效的 HTML 属性,包括 class、style、src 等。
绑定对象可以是 ref 或 reactive 对象的属性。也可以是普通类型的变量,或者是计算属性,v-bind: 可以简写为 “:”
1 | <div :id="dynamicId"></div> |
同时 3.4 以上版本支持同名简写,例如有同名的 id
变量时,可以直接写成:
1 | <div :id></div> |
布尔型 Attribute
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。
1 | <button :disabled="isButtonDisabled">Button</button> |
当
isButtonDisabled为真值或一个空字符串 (即<button disabled="">) 时,元素会包含这个disabledattribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
通过不带参数的 v-bind,你可以将它们绑定到单个元素上:
1 | <script setup> |
计算属性
只读计算属性
1 | <script setup> |
采用计算属性或是在模版中调用一个函数,Vue 都会自动捕获依赖并进行响应式更新,区别在于计算属性会缓存结果,只有当依赖发生变化时才会重新计算,而函数在每次 DOM 更新时都会重新执行。
可写计算属性
1 | <script setup> |
通过 getter 和 setter,计算属性可以实现双向绑定。上面的例子中,fullName 是一个可写计算属性,当 fullName 被修改时,会自动更新 firstName 和 lastName 的值。
当你再运行 fullName.value = ‘John Doe’ 时,setter 会被调用而 firstName 和 lastName 会随之更新。
计算属性是一个
ref,因此也需要.value
获取上一个值
3.4 以上
1 | <script setup> |
或
1 | <script setup> |
计算属性的 getter 应只做计算而没有任何其他的副作用。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!
条件渲染
v-if / v-else-if / v-else
1 | <div v-if="type === 'A'">A</div> |
容易理解,当 type 符合对应分支条件时,只渲染(真正写入
DOM)对应分支的内容
对于多个元素,可以通过 <template> 包裹起来
1 | <template v-if="ok"> |
v-show
1 | <h1 v-show="ok">Hello!</h1> |
v-if vs. v-show
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if 也是惰性的:如果在初次渲染时条件值为
false,则不会做任何事。条件区块只有当条件首次变为
true 时才被渲染。
相比之下,v-show简单许多,元素无论初始条件如何,始终会被渲染,只有
CSS display 属性会被切换。
总的来说,v-if 有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
v-if 和 v-for
当 v-if 和 v-for
同时存在于一个元素上的时候,v-if 会首先被执行。
列表渲染
我们可以使用 v-for
指令基于一个数组来渲染一个列表。v-for 指令的值需要使用
item in items 或 item of items
形式的特殊语法,其中 items 是源数据的数组,而
item 是迭代项的别名:
1 | <script setup> |
也支持 v-for="(item, index) in items",其中
index 是当前迭代项的索引,以及解构语法
v-for="({ message }, index) in items",其中
message 是当前迭代项的属性。
与模板上的 v-if 类似,你也可以在
<template> 标签上使用 v-for
来渲染一个包含多个元素的块。例如:
1 | <ul> |
通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM
元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:
1 | <div v-for="item in items" :key="item.id"> |
事件处理
监听事件
我们可以使用 v-on 指令 (简写为 @) 来监听
DOM 事件,并在事件触发时执行对应的
JavaScript。用法:v-on:click="handler" 或
@click="handler"。
事件处理器 (handler) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
- 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
在内联事件处理器中访问事件参数
1 | <!-- 使用特殊的 $event 变量 --> |
1 | function warn(message, event) { |
事件修饰符
在处理事件时调用 event.preventDefault() 或
event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理
DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用
. 表示的指令后缀,包含以下这些:
.stop.prevent.self.capture.once.passive
1 | <!-- 单击事件将停止传递 --> |
表单输入绑定
单行/多行文本输入框
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript
中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦,v-model
指令帮我们简化了这一步骤:
1 | <p>Message is: {{ message }}</p> |
或
1 | <span>Multiline message is:</span> |
对于需要使用
IME的语言 (中文,日文和韩文等),你会发现v-model不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用v-model。
复选框
单一复选框的 v-model
绑定的值是一个布尔值,表示复选框是否被选中。
1 | <input type="checkbox" id="checkbox" v-model="checked" /> |
我们也可以将多个复选框绑定到同一个数组或集合的值:
1 | <script setup> |
存在数组里的指是
value
单选按钮
1 | <div>Picked: {{ picked }}</div> |
存在
picked里的是 `value
选择器
单选
1 | <div>Selected: {{ selected }}</div> |
如果
v-model表达式的初始值不匹配任何一个选择项,<select>元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个change事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
多选
1 | <div>Selected: {{ selected }}</div> |
绑定的是一个数组,数组中包含所有被选中的值。
修饰符
默认情况下,v-model 会在每次 input 事件后更新数据 (IME
拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次
change 事件后更新数据
1 | <!-- 在 "change" 事件后同步更新而不是 "input" --> |
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number 修饰符来管理输入:
1 | <input v-model.number="age" type="number" /> |
如果你想要默认自动去除用户输入内容中两端的空格,你可以在
v-model 后添加 .trim 修饰符:
1 | <input v-model.trim="msg" /> |
侦听器
watch
可以使用
watch函数在每次响应式状态发生变化时触发回调函数
1 | <script setup> |
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个
ref (包括计算属性)、一个响应式对象、一个
getter 函数、或多个数据源组成的数组:
1 | const x = ref(0); |
但不能侦听一个非响应式对象或响应式对象的属性,因为它们不会触发回调函数,你可以用一个 getter 函数作为 workaround。
1 | // 提供一个 getter 函数 |
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在建侦听器时,立即执行一遍回调。
1 | watch( |
一次性侦听器
1 | watch( |
watchEffect
侦听器的回调使用与源完全相同的响应式状态是很常见的。
1 | const todoId = ref(1); |
而 watchEffect() 允许我们自动跟踪回调的响应式依赖。
1 | watchEffect(async () => { |
watch vs. watchEffect
watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
模板引用
ref 是一个特殊的 attribute,和 v-for 的
key 类似。它允许我们在一个特定的 DOM
元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个
input 元素上,或在一个元素上初始化一个第三方库。
1 | <script setup> |
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问
input,在初次渲染时会是
null。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为
null 的情况:
1 | watchEffect(() => { |
当在 v-for 中使用模板引用时,对应的 ref
中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
1 | <script setup> |
除了使用字符串值作名字,ref attribute
还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
1 | <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" /> |
组件基础
定义
1 | <script setup> |
使用
1 | <script setup> |
传递 props
Props 是一种特别的
attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的
props 列表上声明它。
1 | <script setup> |
当一个 prop 被注册后,可以像这样以自定义 attribute
的形式传递数据给它:
1 | <BlogPost title="My journey with Vue" /> |
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
监听事件
组件实例提供了一个自定义事件系统。父组件可以通过 v-on 或
@ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
1 | <!-- 省略了 <script> --> |
1 | <div :style="{ fontSize: postFontSize + 'em' }"> |
我们可以通过 defineEmits 宏来声明需要抛出的事件,和
defineProps 类似,defineEmits 仅可用于
<script setup> 之中,并且不需要导入,它返回一个等同于
$emit 方法的 emit 函数。它可以被用于在组件的
<script setup> 中抛出事件,因为此处无法直接访问
$emit:
1 | <script setup> |
通过插槽来分配内容
可以通过 Vue 的自定义 <slot>
元素来实现组件的内容分发。父组件可以在子组件的 <slot>
元素中插入任意内容,子组件会将这些内容渲染到 <slot>
元素所在的位置。
1 | <template> |
1 | <AlertBox> Something bad happened. </AlertBox> |
动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
1 | <!-- currentTab 改变时组件也改变 --> |
被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用 is attribute 来创建一般的 HTML 元素。
生命周期钩子
举例来说,onMounted 钩子可以用来在组件完成初始渲染并创建
DOM 节点后运行代码
1 | <script setup> |
生命周期图示