Skip to content

Vue 组件基础

构建工具 + Runtime 的方式运行的 Vue 项目,不支持把模板代码写到 index.html<div id="app"> 容器中。取而代之的,要把模板代码写到后缀名为 .vue 的文件中,这种 .vue 结尾的文件叫做“Vue 单文件组件(英文简称:SFC,Single File Component)”,简称 “Vue 组件”。

把 SFC 组件渲染到页面上

改造 src/App.vue 组件,清空其中的默认内容,并添加 <template> 虚拟元素。Vue 官方规定:SFC 组件的模板结构,必须放到 <template> 元素中:

vue
<!-- 在 template 中定义要渲染的模板结构 -->
<template>
  <h1>count 的值是:</h1>
  <button>+1</button>
</template>

改造 src/main.js 项目入口文件,参考如下三个步骤,把 App.vue 中的模板结构渲染到 index.html 的 id 为 app 的 div 中:

js
import { createApp } from 'vue'
// 1. 导入单文件组件
import App from './App.vue'

// 2. 把单文件组件作为参数,传递给 createApp() 函数
// const app = createApp(SFC单文件组件)
const app = createApp(App)

// 3. 指定要把单文件组件的模板内容,渲染到哪个容器中
app.mount('#app')

最后,请确保 index.html 中 id 为 app 的 div 是一个空元素,否则会导致页面渲染闪烁的问题。

在 SFC 中定义数据

SFC 的 <template> 中需要用到的数据或方法,需要定义到 <script> 标签中。例如:

vue
<template>
  <!-- 在这里定义模板结构 -->
</template>

<script>
// 在这里定义数据和方法等 js 逻辑
</script>

传统方式(不推荐)

在 Vue3 推出 <script setup> 新特性之前,我们需要参考如下的方式在 <script> 中定义数据:

vue
<!-- 在 template 中定义要渲染的模板结构 -->
<template>
  <h1>count 的值是:{{ count }}</h1>
  <button @click="count++">+1</button>
</template>

<script>
import { ref } from 'vue'

// 使用 export default 导出一个对象,
// 在对象中通过 setup 函数向外 return 数据
export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}
</script>

我们发现 <script> 中的第 8 行第 14 行是定义数据的核心代码。除此之外的其它代码行都是冗余的(在多个 .vue 组件中,每次都要重复写一遍)。

<script setup> 语法糖

为了简化 <script> 中的冗余代码,Vue 官方推出了 <script setup> 语法糖:定义在 <script> 中的数据/方法/计算属性等成员,可以直接在 <template> 模板结构中被使用。不必再通过 setup() 向外 return 了。

使用 <script setup> 改造后的代码如下:

vue
<!-- 在 template 中定义要渲染的模板结构 -->
<template>
  <h1>count 的值是:{{ count }}</h1>
  <button @click="count++">+1</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

在 SFC 中定义样式

基本用法

如果需要美化 SFC 的 <template> 模板结构的样式,则需要把样式写到 <style> 标签中。例如:

vue
<template>
  <!-- 在这里定义模板结构 -->
</template>

<script>
// 在这里定义数据和方法等 js 逻辑
</script>

<style>
/* 在这里定义 css 样式,从而美化 template 中的模板结构 */
</style>

示例代码如下:

vue
<template>
  <h1 class="title">count 的值是:{{ count }}</h1>
  <button @click="count++">+1</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<!-- 样式定义到 style 标签中 -->
<style>
.title {
  color: red;
}

button {
  padding: 10px 15px;
  border-radius: 10px;
}
</style>

启用 less 语法

如果想在 style 中使用 less 语法,需要先运行如下的命令安装 less 插件:

bash
npm install -D less

之后,改造 SFC 组件的 <style> 标签,为其添加 lang="less" 属性,即可使用 less 语法编写样式:

less
<style lang="less">
// 定义 less 变量
@font-color: #ff0000;
@btn-padding: 10px 15px;

.title {
  // 使用 CSS 变量
  color: @font-color;
  // 嵌套选择器
  span {
    // 使用 less 的颜色函数
    color: darken(@font-color, 15%);
  }
}

button {
  // 使用 CSS 变量
  padding: @btn-padding;
  border-radius: 10px;
}
</style>

SFC 基本用法总结

  1. 后缀名为 .vue 的组件,叫做 SFC 单文件组件;
  2. 每个单文件组件,由 templatescriptstyle 三部分构成;
  3. 每个 SFC 中必须有一个 templatescript 标签,而 style 标签是可选的;

组件的概念

当学会了 SFC 的基本用法之后,我们再来补充一下“组件”的概念。

组件是对 UI 结构的封装和复用。例如:一个项目的多个页面中都需要用到轮播图,为了实现轮播图的结构和功能的复用,我们可以把轮播图功能封装为独立的可复用的组件。

好处

一次封装,多次使用(不必每次用到轮播图的时候都从 0 到 1 实现一遍)。

在实际应用中,组件常常被组织成层层嵌套树状结构

我们可以看到:

  1. 组件是构成页面布局的基础元素,实际的 HTML 标签都是封装到组件中的;
  2. 每个项目都需要一个根组件(Root 或 App)来承载其它的布局组件;

TIP

在项目的入口模块 main.js 中,传递给 createApp() 函数的那个组件,就是项目中的根组件。

根组件会被渲染(填充)到 index.html 页面的 idapp 的 div 中。

组件的基础用法

在根组件之中渲染其它组件

TIP

  1. 项目中唯一的根组件,一般都直接放到 src/ 目录下,命名为 App.vueRoot.vue
  2. 项目中的其它组件,一般都放在 src/components/ 目录下;

例如:若要把 counter 功能抽离为独立的 Counter.vue 组件,首先,在 src/components/ 目录下新建 Counter.vue 组件:

vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <h1 class="title">count 的值是:{{ count }}</h1>
  <button @click="count++">+1</button>
</template>

<style>
.title {
  color: red;
}

button {
  padding: 10px 15px;
  border-radius: 6px;
}
</style>

清空 App.vue 组件,并按照如下两个步骤导入使用 Counter 组件:

vue
<script setup>
// 1. 导入 Counter 组件
import Counter from './components/Counter.vue'
</script>

<template>
  <!-- 2. 以标签的形式,使用导入的 Counter 组件 -->
  <Counter />
</template>

注意

由于 HTML 的标签名都是小写,因此为了和 HTML 的标签进行区分,建议使用大驼峰的形式定义和使用 Vue 的组件名

实现页面基础布局

美化 App 根组件

清空 App 根组件,为其设置灰色的背景颜色,再让 App 根组件撑满浏览器:

vue
<template>
  <div class="root"></div>
</template>

<style>
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
}

.root {
  height: 100%;
  width: 100%;
  background-color: gray;
}
</style>

由于 htmlbody#app 对应的元素不属于当前的 App 组件,因此建议大家把它们的样式,封装到 src/assets/main.css 全局样式表中:

css
html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
}

并在 src/main.js 中导入全局样式表:

js
import { createApp } from 'vue'
import App from './App.vue'
// 导入全局样式
import './assets/main.css'

const app = createApp(App)
app.mount('#app')

最后 App 组件的结构和样式如下:

vue
<template>
  <div class="root"></div>
</template>

<style>
.root {
  height: 100%;
  width: 100%;
  background-color: gray;
}
</style>

TIP

一般情况下,组件中的 style 标签中只定义当前组件要用到的样式,全局的样式建议封装到 src/assets/main.css 全局样式表中。

创建并使用 Header 组件

src/components/ 目录下新建 Header.vue 组件:

vue
<template>
  <div class="header"></div>
</template>

<style>
.header {
  height: 100px;
  width: 100%;
  background-color: lightblue;
}
</style>

src/App.vue 组件中导入并使用 Header 组件:

vue
<script setup>
// 1. 导入 Header 组件
import Header from './components/Header.vue'
</script>

<template>
  <div class="root">
    <!-- 2. 使用 Header 组件 -->
    <Header />
  </div>
</template>

<style>
.root {
  height: 100%;
  width: 100%;
  background-color: gray;
  /* 3. 优化样式 */
  padding: 10px;
  box-sizing: border-box;
}
</style>

创建并使用 Main 和 Aside 组件

src/components/ 目录下,创建 Main.vue 组件:

vue
<template>
  <div class="main"></div>
</template>

<style>
.main {
  flex: 3;
  background-color: lightgreen;
}
</style>

src/components/ 目录下,创建 Aside.vue 组件:

vue
<template>
  <div class="aside"></div>
</template>

<style>
.aside {
  width: 20%;
  flex: 1;
  background-color: orange;
}
</style>

最后,在 App.vue 组件中,导入并使用 MainAside 组件:

vue
<script setup>
import Header from './components/Header.vue'
// 导入 Main 和 Aside 组件
import Main from './components/Main.vue'
import Aside from './components/Aside.vue'
</script>

<template>
  <div class="root">
    <Header />

    <!-- 页面主体区域 -->
    <div class="content">
      <!-- 主体 -->
      <Main />
      <!-- 侧边栏 -->
      <Aside />
    </div>
  </div>
</template>

<style>
.root {
  height: 100%;
  width: 100%;
  background-color: gray;
  padding: 10px;
  box-sizing: border-box;

  /* flex 布局 */
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.content {
  /* 主体区域的 flex 布局 */
  display: flex;
  gap: 10px;
  flex: 1;
}
</style>

循环渲染 AsideItem 组件

src/components/ 目录下,新建 AsideItem.vue 组件:

vue
<template>
  <div class="aside-item"></div>
</template>

<style>
.aside-item {
  height: 100px;
  width: 100%;
  background-color: lightgray;
}
</style>

Aside 组件中,导入并使用 AsideItem 组件:

vue
<script setup>
// 导入 AsideItem 组件
import AsideItem from './AsideItem.vue'
</script>

<template>
  <div class="aside">
    <!-- 循环渲染3个侧边栏的 Item 项 -->
    <AsideItem v-for="item in 3" />
  </div>
</template>

<style>
.aside {
  width: 20%;
  flex: 1;
  background-color: orange;

  /* 3. 美化样式 */
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
</style>

循环渲染 Article 组件

src/components/ 目录下,新建 Article.vue 组件:

vue
<template>
  <div class="article"></div>
</template>

<style>
.article {
  background-color: lightgray;
  height: 155px;
}
</style>

Main 组件中,导入并使用 Article 组件:

vue
<script setup>
// 1. 导入 Article 组件
import Article from './Article.vue'
</script>

<template>
  <div class="main">
    <!-- 2. 循环渲染2个 Article 组件 -->
    <Article v-for="item in 2" />
  </div>
</template>

<style>
.main {
  flex: 3;
  background-color: lightgreen;

  /* 3. 美化样式 */
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
</style>

组件之间的关系

组件之间的关系分为以下 3 种:

父子关系:直接嵌套的组件称为父子关系,例如 Root 组件是 Header/Main/Aside 的父组件;反之,Header/Main/AsideRoot 的子组件。

后代关系:间接嵌套的组件称为后代关系,RootArticle/Item 组件之间是间接嵌套的后代关系。

兄弟关系:若两个(多个)组件直接属于同一个父组件,则这两个组件称为兄弟关系,例如 Header/Main/Aside 这三个组件是兄弟关系。

注意

Article 和 Item 组件不是兄弟关系,因为它们并不直接属于同一个父组件。

组件的注册

一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。

全局注册

我们可以使用 Vue 应用实例.component() 方法,让组件在当前 Vue 应用中全局可用。语法格式如下:

js
// 1. 导入需要被全局注册的组件
import MyComponent from './App.vue'
// 2. 调用 app 的 .component() 方法注册全局组件,
//    第一个参数,是注册完成之后的“标签名”,
//    第二个参数,是步骤1导入的、需要被注册的那个组件
app.component('MyComponent', MyComponent)

TIP

全局注册的组件,可以在任何组件内直接以标签的形式使用,不必每次单独导入。

全局注册自定义的 Button 组件

src/components/ 目录下,新建 Button.vue 组件:

vue
<template>
  <button>自定义的按钮</button>
</template>

<style lang="less">
button {
  @bg-color: #409eff;
  padding: 8px 15px;
  border: none;
  border-radius: 4px;
  background-color: @bg-color;
  color: #fff;
  transition: background-color 0.1s;

  &:hover {
    background-color: darken(@bg-color, 20%);
    cursor: pointer;
    transition: background-color 0.1s;
  }

  &:active {
    background-color: lighten(@bg-color, 5%);
    cursor: pointer;
    transition: background-color 0.1s;
  }
}
</style>

src/main.js 入口文件中,导入并全局注册 Button 按钮组件:

js
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
// 导入需要被区全局注册的组件
import Button from './components/Button.vue'

const app = createApp(App)
// 调用 app.component() 函数,注册全局组件
app.component('MyButton', Button)

app.mount('#app')

温馨提示

app.component() 方法的调用,必须在 createApp() 之后,否则 app 实例还没有被创建!

局部注册

全局注册虽然很方便,但有以下两个问题:

全局注册组件存在的问题

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 js 文件中。
  2. 全局注册在大型项目中使组件的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好

在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用:

vue
<script setup>
import ComponentA from './ComponentA.vue'
</script>

<template>
  <ComponentA />
</template>

组件名格式

再来强调一遍,Vue 官方推荐使用大驼峰(PascalCase)命名法来注册组件的标签名称。这是因为:

  1. 大驼峰命名法是合法的 JavaScript 标识符,这使得在 js 中导入和注册组件都很方便,同时代码编辑器能够提供较好的代码自动补全功能。
  2. 大驼峰命名法能更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。

因此,今后在 SFC 中导入和使用组件时,推荐大家都采用大驼峰命名法。

SFC 中的样式

演示样式冲突的问题

默认情况下,在组件中声明的 <style> 属于全局样式,这些样式除了会影响组件自身的元素,还会影响到其它组件中的元素。因为这些组件经过 Vue 编译之后,都会被呈现在唯一的 index.html 页面中,所以很容易出现样式冲突的问题。

例如:在 src/components/AsideItem.vue 组件中添加一个 <button> 标签,刷新浏览器后我们发现,这个 button 的样式被污染了。污染的来源是全局注册的 <MyButton> 组件:

vue
<template>
  <div class="aside-item">
    <button>无辜的 button</button>
  </div>
</template>

<style>
.aside-item {
  height: 100px;
  width: 100%;
  background-color: lightgray;
}
</style>

解决样式冲突的问题

解决样式冲突的方式有很多种,在这里,我们使用自定义属性 + 属性选择器的方式来解决组件之间样式冲突的问题。实现思路如下:

  1. 为每个 SFC 组件分配唯一的自定义属性名
  2. 把自定义的属性名,添加给当前组件的每一个元素
  3. 给当前组件的样式选择器,添加一个交集的属性选择器

例如,给 src/components/Button.vue 指定一个自定义属性 data-v-001,然后把 Button 组件中的模板结构和样式修改如下:

vue
<template>
  <!-- 注意:每个元素身上都添加了一个 data-v-* 的自定义属性 -->
  <button data-v-001>自定义的按钮</button>
  <h3 class="title" data-v-001>这是 MyButton 中的标题</h3>
</template>

<style lang="less">
/* 注意:在定义样式选择器的时候,追加一个“交集的属性选择器” */
/*       从而让当前组件中的样式,只在当前组件内生效,不会污染到其它组件 */
button[data-v-001] {
  @bg-color: #409eff;
  padding: 8px 15px;
  border: none;
  border-radius: 4px;
  background-color: @bg-color;
  color: #fff;
  transition: background-color 0.1s;

  &:hover {
    background-color: darken(@bg-color, 20%);
    cursor: pointer;
    transition: background-color 0.1s;
  }

  &:active {
    background-color: lighten(@bg-color, 5%);
    cursor: pointer;
    transition: background-color 0.1s;
  }
}

/* 注意:在定义样式选择器的时候,追加一个“交集的属性选择器” */
/*       从而让当前组件中的样式,只在当前组件内生效,不会污染到其它组件 */
.title[data-v-001] {
  color: red;
}
</style>

这样,就成功的解决了组件之间的样式冲突问题。因为每个组件中的样式,都有添加了唯一的属性选择器,从而保证了当前组件的样式只会影响自身的元素。

其实 Vue 为了简化用户的编码复杂度,为 <style> 提供了 scoped 选项。Vue 在编译 SFC 模板的时候,如果发现 <style scoped> 的写法,就会自动给当前组件生成一个唯一的 data-v-* 属性选择器,并自动把属性选择器添加到每个元素和每个 CSS 选择器上。

最佳实践

今后再创建 SFC 组件的时候,一定要为每个组件的 <style> 添加 scoped 选项,从而防止组件之间的样式冲突问题!

根元素与 scoped *

在 Vue3 中的 SFC 组件内,<template> 区域允许存在多个根元素,同时,也支持 Vue2 中的唯一根元素的写法。当父子组件同时应用 scoped 时:

  1. 如果子组件的 <template> 中存在多个根元素,不会把父组件的 data-v-* 添加给子组件的根元素。
  2. 如果子组件的 <template> 中只有唯一的根元素,则会把父组件的 data-v-* 添加给子组件的根元素。

TIP

请记住这个特性,因为它是导致 Vue 路由过渡动画产生 Bug 的本质原因。

深度选择器

当父组件开启了 <style> 标签的 scoped 选项之后,默认情况下,父组件中编写的样式将无法影响到子组件中的“非根元素”,这完全符合防止组件之间样式冲突的需求。

但是,这也产生了一个负面的作用:在不改动子组件代码的情况下,父组件中无法微调子组件中的任何样式(子组件的根元素除外)

解决方案很简单,使用 :deep() 这个伪类,把子组件的选择器包裹起来即可。语法格式如下:

css
/* 父组件的 style 区域 */
:deep(子组件的选择器) {
  /* 样式 */
}

例如:在 Header 组件中,更改 Button 按钮的背景颜色:

vue
<template>
  <div class="header">
    <MyButton />
  </div>
</template>

<style scoped>
.header {
  height: 100px;
  width: 100%;
  background-color: lightblue;
}

/* 把子组件的选择器,使用 :deep() 包裹起来 */
:deep(button) {
  background-color: blueviolet !important;
}
</style>

最终生成的选择器是:

css
/* 注意 button 前面的“空格” */
[data-v-c970699f] button {
  background-color: blueviolet !important;
}

如果在 Header 父组件中没有使用 :deep() 伪类,则最终生成的选择器是 button[data-v-c970699f],此时无法选中子组件中的 button 按钮。因为子组件的 data-v 值是 data-v-3c9d0845

CSS 中的 v-bind()

SFC 中的 <style> 标签支持使用 v-bind() 这个 CSS 函数将响应式数据绑定为 CSS 属性的值。例如把响应式数据 count 绑定为 h3 元素的 font-size 值。

绑定行内的 style 样式

在不使用 CSS 的 v-bind() 函数之前,需要按照如下的方式实现功能:

vue
<script setup>
import { ref } from 'vue'
// 定义响应式数据
const size = ref(16)
</script>

<template>
  <button @click="size++">自定义的按钮</button>
  <!-- 绑定行内的 style 样式对象 -->
  <h3 class="title" :style="{ fontSize: size + 'px' }">这是 MyButton 中的标题</h3>
</template>

<style lang="less" scoped>
button {
  @bg-color: #409eff;
  padding: 8px 15px;
  border: none;
  border-radius: 4px;
  background-color: @bg-color;
  color: #fff;
  transition: background-color 0.1s;

  &:hover {
    background-color: darken(@bg-color, 20%);
    cursor: pointer;
    transition: background-color 0.1s;
  }

  &:active {
    background-color: lighten(@bg-color, 5%);
    cursor: pointer;
    transition: background-color 0.1s;
  }
}

.title {
  color: red;
}
</style>

使用 CSS 中的 v-bind() 函数

在使用了 CSS 的 v-bind() 函数之后,可以按照如下的方式实现功能:

vue
<script setup>
import { ref } from 'vue'
// 定义响应式数据
const size = ref(16)
</script>

<template>
  <button @click="size++">自定义的按钮</button>
  <h3 class="title">这是 MyButton 中的标题</h3>
</template>

<style lang="less" scoped>
button {
  @bg-color: #409eff;
  padding: 8px 15px;
  border: none;
  border-radius: 4px;
  background-color: @bg-color;
  color: #fff;
  transition: background-color 0.1s;

  &:hover {
    background-color: darken(@bg-color, 20%);
    cursor: pointer;
    transition: background-color 0.1s;
  }

  &:active {
    background-color: lighten(@bg-color, 5%);
    cursor: pointer;
    transition: background-color 0.1s;
  }
}

.title {
  color: red;
  font-size: v-bind("size + 'px'");
}
</style>

注意

CSS 的 v-bind() 函数中支持 JavaScript 表达式(需要用引号包裹起来)。

原理

  • 实际的值会被编译成哈希化的 CSS 自定义属性,因此 CSS 本身仍然是静态的。
  • 自定义属性会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式地更新。

选项式 API & 组合式 API *

在 Vue3 的 SFC 单文件组件中,同时支持使用选项式 API组合式 API 来封装组件的 JS 逻辑。

选项式 API

选项式 API 是从 Vue2 就存在的特性,为了让 Vue2 的用户友好地迁移到 Vue3,因此 Vue3 中依然保留了选项式 API 的用法。选项式 API 的语法如下:

vue
<template></template>

<script>
// 在导出的这个对象中,提供了一系列的内置选项(选项名称是固定写法),
// 用来声明组件的 数据、方法、计算属性、侦听器 等
export default {
  name: '',
  data() {},
  methods: {},
  computed: {},
  watch: {},
  props: {},
  components: {},
  directives: {},
  emits: {}
}
</script>

<style></style>

在选项式 API 中,不同的内容要分别放到不同的选项下。例如:

数据要放到 data 选项中
方法要放到 methods 选项中
侦听器要放到 watch 选项中
计算属性要放到 computed 选项中
其它选项不再逐一说明...

优势

这种内置的选项能够简化初学者的上手难度,这也是为什么 Vue2 能够迅速发展为前端圈的流行框架的原因之一。

这种内置的选项式 API 也带来了一些负面的影响:一个完整的功能被支离破碎的拆分到了不同的选项中,难以在多个组件之间复用,同时也增大了维护的难度。 这个问题在大型的复杂项目中尤为突出。

请参考如下的截图理解这个问题(颜色相同的代码块表示一个完整功能的组成部分):

快速体验选项式 API 的用法

下面的代码演示了如何基于选项式 API 实现“count 自增”的功能:

vue
<template>
  <h1>快速体验选项式 API 的用法</h1>
  <hr />

  <h3>count 的值是:{{ count }}</h3>
  <p>count 乘 2 的值是:{{ countPlus2 }}</p>
  <button @click="add">+1</button>
</template>

<script>
export default {
  data() {
    // 数据的选项
    return {
      count: 0
    }
  },
  methods: {
    // 方法的选项
    add() {
      this.count++
    }
  },
  computed: {
    // 计算属性的选项
    countPlus2() {
      return this.count * 2
    }
  }
}
</script>

如果,我们想要再添加一个“设置网页背景色”的功能,需要把刚才的代码修改如下:

vue
<template>
  <h1>快速体验选项式 API 的用法</h1>
  <hr />

  <h3>count 的值是:{{ count }}</h3>
  <p>count 乘 2 的值是:{{ countPlus2 }}</p>
  <button @click="add">+1</button>

  <hr />

  <input type="color" v-model="bgColor" />
  <button @click="setBgColor">设置网页的背景颜色</button>
</template>

<script>
export default {
  data() {
    // 数据的选项
    return {
      count: 0, // 计数器的数据
      bgColor: '#ffffff' // 背景色的数据
    }
  },
  methods: {
    // 方法的选项
    add() {
      // 计数器的处理函数
      this.count++
    },
    setBgColor() {
      // 背景色的处理函数
      window.document.body.style.backgroundColor = this.bgColor
    }
  },
  computed: {
    // 计算属性的选项
    countPlus2() {
      return this.count * 2
    }
  }
}
</script>

TIP

选项式 API 会导致完整的功能被拆分到不同的选项中,增加了维护的成本,而且降低了代码的复用性。

组合式 API

组合式 API 是 Vue3 中特有的新功能,它突破了选项式 API 的限制,能够让程序员自由组合想要实现的功能。

组合式 API 的本质函数式编程,把定义响应式数据、侦听器、计算属性等功能封装为可独立调用的 API 函数

例如我们之前所学的、从 Vue 中按需导入的 ref()reactive()computed()watch()watchEffect() 等这些函数,都是 Vue3 中新增的组合式 API。

使用组合式 API 改写刚才的案例

我们可以把刚才的案例,改写成下面的代码:

vue
<template>
  <h1>快速体验选项式 API 的用法</h1>
  <hr />

  <h3>count 的值是:{{ count }}</h3>
  <p>count 乘 2 的值是:{{ countPlus2 }}</p>
  <button @click="add">+1</button>

  <hr />

  <input type="color" v-model="bgColor" />
  <button @click="setBgColor">设置网页的背景颜色</button>
</template>

<script setup>
import { ref, computed } from 'vue'

// 计数器的功能
const count = ref(0)
const countPlus2 = computed(() => count.value * 2)
const add = () => count.value++

// 改变网页背景色的功能
const bgColor = ref('#ffffff')
const setBgColor = () => (document.body.style.backgroundColor = bgColor.value)
</script>

TIP

改写成组合式 API 的写法之后,我们发现代码更加聚合,也减少了不必要的样板代码。

封装自定义 hook

组合式 API 的另一个好处是:可以提高代码的复用性。只需要把通用的逻辑代码封装到 use 开头的函数中,即可实现代码的复用。这种以 use 开头的自定义函数叫做“自定义 hook”。

例如,把“改变网页背景色”的功能和“计数器”的功能,封装到 src/hooks/index.js 模块中:

js
import { ref, computed } from 'vue'

// 把设置网页背景色的功能封装为可复用的 hook
// 注意:自定义 hook 必须以 useXXX 开头
export const useBackgroundColor = (initColor = '#ffffff') => {
  const bgColor = ref(initColor)
  const setBgColor = () => (document.body.style.backgroundColor = bgColor.value)

  return [bgColor, setBgColor]
}

// 把计数器功能封装为可复用的 hook
export const useCounter = (initCount = 0) => {
  const count = ref(initCount)
  const countPlus2 = computed(() => count.value * 2)
  const add = () => (count.value += 1)

  return [count, countPlus2, add]
}

今后,在任何 SFC 组件中凡是需要用到这两个功能,只需导入对应的 hook 即可:

vue
<template>
  <h1>快速体验选项式 API 的用法</h1>
  <hr />

  <h3>count 的值是:{{ count }}</h3>
  <p>count 乘 2 的值是:{{ countPlus2 }}</p>
  <button @click="add">+1</button>

  <hr />

  <input type="color" v-model="bgColor" />
  <button @click="setBgColor">设置网页的背景颜色</button>
</template>

<script setup>
// import { ref, computed } from 'vue'
import { useBackgroundColor, useCounter } from './hooks'

// 计数器的功能
// const count = ref(0)
// const countPlus2 = computed(() => count.value * 2)
// const add = () => count.value++
const [count, countPlus2, add] = useCounter()

// 改变网页背景色的功能
// const bgColor = ref('#ffffff')
// const setBgColor = () => (document.body.style.backgroundColor = bgColor.value)
const [bgColor, setBgColor] = useBackgroundColor()
</script>

总结

在今后的开发中,推荐大家优先使用组合式 API 进行 Vue3 的开发,选项式 API 了解即可。

天不生夫子,万古长如夜