1 Class 与 Style 绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
a.绑定 HTML class
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
1 | <div :class="{ active: isActive }"></div> |
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。举例来说,下面这样的状态
1 | const isActive = ref(true) |
配合以下模板:
1 | <div |
渲染的结果会是:
1 | <div class="static active"></div> |
当 isActive
或者 hasError
改变时,class 列表会随之更新。举例来说,如果 hasError
变为 true
,class 列表也会变成 "static active text-danger"
。
绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
1 | const classObject = reactive({ |
1 | <div :class="classObject"></div> |
这将渲染:
1 | <div class="active"></div> |
我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
1 | const isActive = ref(true) |
1 | <div :class="classObject"></div> |
b.绑定数组
我们可以给 :class
绑定一个数组来渲染多个 CSS class:
1 | const activeClass = ref('active') |
1 | <div :class="[activeClass, errorClass]"></div> |
渲染的结果是:
1 | <div class="active text-danger"></div> |
如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:
1 | <div :class="[isActive ? activeClass : '', errorClass]"></div> |
errorClass
会一直存在,但 activeClass
只会在 isActive
为真时才存在。
然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象:
1 | <div :class="[{ active: isActive }, errorClass]"></div> |
c.在组件上使用
对于只有一个根元素的组件,当你使用了 class
attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。
举例来说,如果你声明了一个组件名叫 MyComponent
,模板如下:
1 | <!-- 子组件模板 --> |
在使用时添加一些 class:
1 | <!-- 在使用组件时 --> |
渲染出的 HTML 为:
1 | <p class="foo bar baz boo">Hi!</p> |
Class 的绑定也是同样的:
1 | <MyComponent :class="{ active: isActive }" /> |
当 isActive
为真时,被渲染的 HTML 会是:
1 | <p class="foo bar active">Hi!</p> |
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs
属性来实现指定:
1 | <!-- MyComponent 模板使用 $attrs 时 --> |
1 | <MyComponent class="baz" /> |
这将被渲染为:
1 | <p class="baz">Hi!</p> |
d 绑定内联样式
绑定对象
:style
支持绑定 JavaScript 对象值,对应的是 **HTML 元素的 style
属性**:
1 | const activeColor = ref('red') |
1 | <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> |
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
1 | <div :style="{ 'font-size': fontSize + 'px' }"></div> |
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
1 | const styleObject = reactive({ |
1 | <div :style="styleObject"></div> |
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
2条件渲染
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
1 | <h1 v-if="awesome">Vue is awesome!</h1> |
你也可以使用 v-else
为 v-if
添加一个“else 区块”。
1 | <button @click="awesome = !awesome">Toggle</button> |
v-else-if
提供的是相应于 v-if
的“else if 区块”。它可以连续多次重复使用:
1 | <div v-if="type === 'A'"> |
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
1 | <h1 v-show="ok">Hello!</h1> |
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
3列表渲染
我们可以使用 v-for
指令基于一个数组来渲染一个列表。v-for
指令的值需要使用 item in items
形式的特殊语法,其中 items
是源数据的数组,而 item
是迭代项的别名:
1 | const items = ref([{ message: 'Foo' }, { message: 'Bar' }]) |
1 | <li v-for="item in items"> |
在 v-for
块中可以完整地访问父作用域内的属性和变量。v-for
也支持使用可选的第二个参数表示当前项的位置索引。
1 | const parentMessage = ref('Parent') |
1 | <li v-for="(item, index) in items"> |
v-for
变量的作用域和下面的 JavaScript 代码很类似:
1 | const parentMessage = 'Parent' |
与模板上的 v-if
类似,你也可以在 <template>
标签上使用 v-for
来渲染一个包含多个元素的块。例如:
1 | <ul> |
4事件处理
a监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler"
或 @click="handler"
。
事件处理器 (handler) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
b内联事件处理器(直接用)
内联事件处理器通常用于简单场景,例如:
1 | const count = ref(0) |
1 | <button @click="count++">Add 1</button> |
c方法事件处理器(通过方法调用)
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
举例来说:
1 | const name = ref('Vue.js') |
1 | <!-- `greet` 是上面定义过的方法名 --> |
d在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
1 | <!-- 使用特殊的 $event 变量 --> |
1 | function warn(message, event) { |
e事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
1 | <!-- 单击事件将停止传递 --> |
5 表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
1 | <input |
v-model
指令帮我们简化了这一步骤:
1 | <input v-model="text"> |
另外,v-model
还可以用于各种不同类型的输入,<textarea>
、<select>
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>
和<textarea>
元素会绑定value
property 并侦听input
事件; <input type="checkbox">
和<input type="radio">
会绑定checked
property 并侦听change
事件;<select>
会绑定value
property 并侦听change
事件。
文本
1 | <p>Message is: {{ message }}</p> |
多行文本
1 | <span>Multiline message is:</span> |
复选框
单一的复选框,绑定布尔类型值:
1 | <input type="checkbox" id="checkbox" v-model="checked" /> |
我们也可以将多个复选框绑定到同一个数组或集合的值:
1 | const checkedNames = ref([]) |
1 | <div>Checked names: {{ checkedNames }}</div> |
选择器
单个选择器的示例如下:
1 | <div>Selected: {{ selected }}</div> |
多选 (值绑定到一个数组):
1 | <div>Selected: {{ selected }}</div> |
选择器的选项可以使用 v-for
动态渲染:
1 | const selected = ref('A') |
1 | <select v-model="selected"> |
6生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
a注册周期钩子
举例来说,onMounted
钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
1 | <script setup> |
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 [onMounted](https://cn.vuejs.org/api/composition-api-lifecycle.html#onmounted)
、[onUpdated](https://cn.vuejs.org/api/composition-api-lifecycle.html#onupdated)
和 **[onUnmounted](https://cn.vuejs.org/api/composition-api-lifecycle.html#onunmounted)
**。所有生命周期钩子的完整参考及其用法请参考 **API 索引**。
当调用 onMounted
时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:
1 | setTimeout(() => { |
注意这并不意味着对 onMounted
的调用必须放在 setup()
或 <script setup>
内的词法上下文中。onMounted()
也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup()
就可以。
b.生命周期图示
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
7 侦听器
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 [watch](https://cn.vuejs.org/api/reactivity-core.html#watch)
函数在每次响应式状态发生变化时触发回调函数:
1 | <script setup> |
侦听数据源类型
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
1 | const x = ref(0) |
注意,你不能直接侦听响应式对象的属性值,例如:
1 | const obj = reactive({ count: 0 }) |
这里需要用一个返回该属性的 getter 函数:
1 | // 提供一个 getter 函数 |
8 模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
1 | <input ref="input"> |
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key
类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
a.访问模板引用
为了通过组合式 API 获得该模板引用,我们需要声明一个匹配模板 ref attribute 值的 ref:
1 | <script setup> |
如果不使用 <script setup>
,需确保从 setup()
返回 ref:
1 | export default { |
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input
,在初次渲染时会是 null
。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null
的情况:
1 | watchEffect(() => { |
b函数模板引用
除了使用字符串值作名字,ref
attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
1 | <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> |
注意我们这里需要使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null
。你当然也可以绑定一个组件方法而不是内联函数。
c组件上的 ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
1 | <script setup> |
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
1 | <script setup> |
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
9.组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。如果你想知道 Vue 组件与原生 Web Components 之间的关系
a定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件 (简称 SFC):
1 | <script setup> |
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:(好像不太常用这个形式)
1 | import { ref } from 'vue' |
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template>
元素),Vue 将会使用其内容作为模板来源。
上面的例子中定义了一个组件,并在一个 .js
文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。
b使用组件
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
1 | <script setup> |
通过 <script setup>
,导入的组件都在模板中直接可用。
当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。。
组件可以被重用任意多次:
1 | <h1>Here is a child component!</h1> |
你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count
。这是因为每当你使用一个组件,就创建了一个新的实例。
在单文件组件中,推荐为子组件使用 PascalCase
的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 />
来关闭一个标签。
如果你是直接在 DOM 中书写模板 (例如原生 <template>
元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case
形式并显式地关闭这些组件的标签。
c传递 props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps
宏:
1 | <!-- BlogPost.vue --> |
defineProps
是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props:
1 | const props = defineProps(['title']) |
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:
1 | <BlogPost title="My journey with Vue" /> |
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
1 | const posts = ref([ |
这种情况下,我们可以使用 v-for
来渲染它们:
1 | <BlogPost |
d监听事件
让我们继续关注我们的 <BlogPost>
组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。
在父组件中,我们可以添加一个 postFontSize
ref 来实现这个效果:
1 | const posts = ref([ |
在模板中用它来控制所有博客文章的字体大小:
1 | <div :style="{ fontSize: postFontSize + 'em' }"> |
然后,给 <BlogPost>
组件添加一个按钮:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
1 | <BlogPost |
子组件可以通过调用内置的 **[$emit
方法](https://cn.vuejs.org/api/component-instance.html#emit)**,通过传入事件名称来抛出一个事件:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
我们可以通过 [defineEmits](https://cn.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)
宏来声明需要抛出的事件:
1 | <!-- BlogPost.vue --> |
这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
和 defineProps
类似,defineEmits
仅可用于 <script setup>
之中,并且不需要导入,它返回一个等同于 $emit
方法的 emit
函数。它可以被用于在组件的 <script setup>
中抛出事件,因为此处无法直接访问 $emit
:
1 | <script setup> |
e通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
1 | <AlertBox> |
我们期望能渲染成这样:
这可以通过 Vue 的自定义 <slot>
元素来实现:
1 | <template> |
今天学到这里,下课!