Skip to content

18 Vue3 入门

  • Vue3 是 Vue.js 的下一个主要版本,它的目标是提供更快的渲染速度、更小的体积、更好的 TypeScript 支持、更好的代码组织和更好的开发体验。
  • 官方文档:Vue.js - 渐进式 JavaScript 框架 | Vue.js

Vue2 和 Vue3

API 风格

Vue 的组件可以按两种不同的风格书写:选项式 API组合式 API

vue
<script>
  export default {
    // data() 返回的属性将会成为响应式的状态
    // 并且暴露在 `this` 上
    data() {
      return {
        count: 0,
      }
    },

    // methods 是一些用来更改状态与触发更新的函数
    // 它们可以在模板中作为事件处理器绑定
    methods: {
      increment() {
        this.count++
      },
    },

    // 生命周期钩子会在组件生命周期的各个不同阶段被调用
    // 例如这个函数就会在组件挂载完成后被调用
    mounted() {
      console.log(`The initial count is ${this.count}.`)
    },
  }
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>
vue
<script setup>
  import { ref, onMounted } from 'vue'

  // 响应式状态
  const count = ref(0)

  // 用来修改状态、触发更新的函数
  function increment() {
    count.value++
  }

  // 生命周期钩子
  onMounted(() => {
    console.log(`The initial count is ${count.value}.`)
  })
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

选项式 API (Options API)

  • 使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethodsmounted
  • 选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

组合式 API (Composition API)

  • 通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。
  • 在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。

该选哪一个

  • 两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。
  • 选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
  • 组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
  • 大致建议:
    • 在学习的过程中,推荐采用更易于自己理解的风格。再强调一下,大部分的核心概念在这两种风格之间都是通用的。熟悉了一种风格以后,你也能够很快地理解另一种风格。
    • 在生产项目中:
      • 当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。
      • 当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。
  • 相比 Vue2 选项式 API,Vue3 组合式 API 有以下优点:
    • 代码量变少
    • 分散式维护变成集中式维护

Vue3 的优势

  1. 更容易维护
    • 组合式 API
    • 更好的 TypeScript 支持
  2. 更快的速度
    • 重写 diff 算法
    • 模版编译优化
    • 更高效的组件初始化
  3. 更小的体积
    • 良好的 TreeShaking
    • 按需引入
  4. 更优的数据响应式
    • Proxy

创建 Vue3 应用

快速上手 | Vue.js

安装 Node.js

安装最新 LTS 版本的 Node.js

bash
scoop install nodejs-lts
fnm install --lts

使用 create-vue 创建项目

bash
pnpm create vue@latest

这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。

bash
 pnpm create vue@latest
../.pnpm-store/v3/tmp/dlx-12704          |   +1 +
../.pnpm-store/v3/tmp/dlx-12704          | Progress: resolved 1, reused 0, downloaded 1, added 1, done

Vue.js - The Progressive JavaScript Framework

 请输入项目名称: ... vue3-demo
 是否使用 TypeScript 语法? ... /
 是否启用 JSX 支持? ... /
 是否引入 Vue Router 进行单页面应用开发? ... /
 是否引入 Pinia 用于状态管理? ... /
 是否引入 Vitest 用于单元测试? ... /
 是否要引入一款端到端(End to End)测试工具? » 不需要
 是否引入 ESLint 用于代码质量检测? ... /
 是否引入 Prettier 用于代码格式化? ... /

正在构建项目 E:\code\vue3-demo...

项目构建完成,可执行以下命令:

  cd vue3-demo
  pnpm install
  pnpm format
  pnpm dev

 cd vue3-demo && pnpm i
Packages: +152
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Progress: resolved 187, reused 134, downloaded 18, added 152, done
node_modules/.pnpm/esbuild@0.19.12/node_modules/esbuild: Running postinstall script, done in 52ms

dependencies:
+ vue 3.4.15

devDependencies:
+ @rushstack/eslint-patch 1.7.2
+ @vitejs/plugin-vue 5.0.3
+ @vue/eslint-config-prettier 8.0.0 (9.0.0 is available)
+ eslint 8.56.0
+ eslint-plugin-vue 9.21.1
+ prettier 3.2.4
+ vite 5.0.12

Done in 5.5s

熟悉项目和关键文件

bash
 lsd --tree --icon-theme unicode --group-directories-first -I node_modules
📂 .
├── 📂 public               # 存放公共资源
   └── 📄 favicon.ico
├── 📂 src
   ├── 📂 assets           # 项目的静态资源,比如 CSS 文件和图像文件
   ├── 📄 base.css
   ├── 📄 logo.svg
   └── 📄 main.css
   ├── 📂 components       # 存放项目的 Vue 组件文件
   ├── 📄 App.vue          # 根组件,SFC 单文件组件
   └── 📄 main.js          # 入口文件,初始化 Vue 应用并挂载根组件、createApp 函数创建应用实例
├── 📄 .eslintrc.cjs        # ESLint 的配置文件,用于定义代码规范和检查
├── 📄 .gitignore
├── 📄 .prettierrc.json     # Prettier 的配置文件
├── 📄 index.html           # 单页入口,提供 id 为 app 的挂载点
├── 📄 jsconfig.json
├── 📄 package.json
├── 📄 pnpm-lock.yaml
├── 📄 README.md
└── 📄 vite.config.js       # Vite 构建工具的配置文件

和 Vue2 项目相比,Vue3 项目的目录结构没有太大变化,但是有一些文件的内容发生了变化。

  • main.js 中使用 createApp 函数创建应用实例,而不是 new Vue
  • App.vue
    • 组合式 API:<script> 标签中使用 setup 属性,而不是 export default
    • 脚本部分使用了 refonMounted 等组合式 API 函数,而不是 datamethods 等选项式 API。
    • 脚本 script 和模板 template 顺序调整。
    • 模板 template 不再要求唯一根元素。
  • vite.config.js 是 Vite 构建工具的配置文件,而不是 vue.config.js
  • package.json 中的依赖版本发生了变化。项自包文件核心依赖项变成了 Vue3.x 和 vite

IDE 开发工具

  • 推荐使用的 IDE 是 VSCode,配合 Vue 语言特性 (Volar) 插件。该插件提供了语法高亮、TypeScript 支持,以及模板内表达式与组件 props 的智能提示。

    Tip

    Volar 取代了之前为 Vue 2 提供的官方 VSCode 扩展 Vetur。如果之前已经安装了 Vetur,请确保在 Vue 3 的项目中禁用它。

  • WebStorm 同样也为 Vue 的单文件组件提供了很好的内置支持。其他的 JetBrains IDE 也同样可以通过一个免费插件支持。从 2023.2 版开始,WebStorm 和 Vue 插件内置了对 Vue 语言服务器的支持。你可以在设置 > 语言和框架 > TypeScript > Vue 下将 Vue 服务设置为在所有 TypeScript 版本上使用 Volar 集成。默认情况下,Volar 将用于 TypeScript 5.0 及更高版本。

  • 其他支持语言服务协议 (LSP) 的 IDE 也可以通过 LSP 享受到 Volar 所提供的核心功能:

浏览器开发者插件

代码规范

  • Vue 团队维护着 eslint-plugin-vue 项目,它是一个 ESLint 插件,会提供 SFC 相关规则的定义。
  • 使用步骤:
    1. pnpm add -D eslint eslint-plugin-vue,然后遵照 eslint-plugin-vue指引进行配置。
    2. 启用 ESLint IDE 插件,比如 ESLint for VSCode,然后你就可以在开发时获得规范检查器的反馈。这同时也避免了启动开发服务器时不必要的规范检查。
    3. 将 ESLint 格式检查作为一个生产构建的步骤,保证你可以在最终打包时获得完整的规范检查反馈。
    4. (可选) 启用类似 lint-staged一类的工具在 Git commit 提交时自动执行规范检查。。

组合式 API

API 参考 | Vue.js

setup()

  1. 执行时机,比 beforeCreate 还要早
  2. setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行
  3. setup() 函数中返回的对象会暴露给模板和组件实例 (需要在 setup 最后 return,才能模板中应用).
  4. 通过 setup 语法糖可以简化代码,不再需要 return 返回。
    • <script setup> 中的代码会在每次组件实例被创建的时候执行。
    • 任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用。
    • import 导入的内容也会以同样的方式暴露。
    • <script setup> 范围里的值也能被直接作为自定义组件的标签名使用。
    • 响应式状态需要明确使用响应式 API 来创建。和 setup() 函数的返回值一样,ref 在模板中使用的时候会自动解包
vue
<script setup>
// 1. 执行时机,比 beforeCreate 还要早
// 2. `setup()` 自身并不含对组件实例的访问权,即在 `setup()` 中访问 `this` 会是 `undefined`。 `setup()` 是一个新的组合 API,它是在组件实例创建之前执行的,所以在 `setup()` 中无法访问到 `this`,因为 `this` 是在组件实例创建之后才会有的
// 3. 数据 和 函数,需要在 setup 最后 return,才能模板中应用
const msg = 'Hello Vue 3.0'
const logMessage = () => console.log(msg)
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <button @click="logMessage">
      打印
    </button>
  </div>
</template>
vue
<script>
// 1. 执行时机,比 beforeCreate 还要早
// 2. `setup()` 自身并不含对组件实例的访问权,即在 `setup()` 中访问 `this` 会是 `undefined`。 `setup()` 是一个新的组合 API,它是在组件实例创建之前执行的,所以在 `setup()` 中无法访问到 `this`,因为 `this` 是在组件实例创建之后才会有的
// 3. 数据 和 函数,需要在 setup 最后 return,才能模板中应用
export default {
  name: 'App',

  setup() {
    // setup 函数中的 this 是 undefined
    console.log('setup 函数执行了', this)
    const msg = 'Hello Vue 3.0'
    const logMessage = () => console.log(msg)

    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      msg,
      logMessage,
    }
  },
  beforeCreate() {
    console.log('beforeCreate 函数执行了')
  },
}
</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <button @click="logMessage">
      打印
    </button>
  </div>
</template>

reactive()

  • reactive():接收一个对象类型的数据,返回一个响应式的对象。接受简单类型的数据不会报错,但是不会变成响应式的。
vue
<script setup>
import { reactive } from 'vue'

// 1. reactive() 只能接收一个对象类型的数据,返回一个响应式的对象
const book = reactive({ title: 'Vue 3.0' })
const changeTitle = () => book.title = 'Vue 3 指南'

// 2. reactive() 接收一个基础类型的数据,但不能返回一个响应式的数据
const count = reactive(0)
// 浏览器会显示警告信息:value cannot be made reactive: 0

const increment = () => count.value++
// 浏览器会显示错误信息:App.vue: value cannot be made reactive: 0
</script>

<template>
  <div>
    <h2>{{ book }}</h2>
    <button @click="changeTitle">
      Change Title
    </button>
    <hr>
    <h2>{{ count }}</h2>
    <button @click="increment">
      Increment
    </button>
  </div>
</template>

ref()

  • ref(): 接收简单类型 或 复杂类型,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
  • ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。
  • 如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,请使用 shallowRef() 来替代。
  • 在模板中使用 ref 时,我们需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
vue
<script setup>
import { ref } from 'vue'

const book = ref({ title: 'Vue 3.0' })
const changeTitle = () => book.value.title = 'Vue 3 指南'

const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <div>
    <h2>{{ book.title }}</h2>
    <button @click="changeTitle">
      Change Title
    </button>
    <hr>
    <h2>{{ count }}</h2>
    <button @click="increment">
      Increment
    </button>
  </div>
</template>

reactive() 对比 ref()

  1. 都是用来生成响应式数据
  2. 不同点
    • reactive 不能处理简单类型的数据
    • ref 参数类型支持更好,但是必须通过 .value 做访问修改
    • ref 函数内部的实现依赖于 reactive 函数
  3. 在实际工作中的推荐
    • 推荐使用 ref 函数,减少记忆负担,小兔鲜项目都使用 ref

computed()

  • computed():接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。
  • 它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。
vue
<script setup>
import { computed, ref } from 'vue'

const array = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
const book = ref({ title: 'Vue 3.0' })

const plusOneNumber = computed(() => array.value.map(n => n + 1))
const evenNumbers = computed(() => array.value.filter(n => n % 2 === 0))
const bookTitle = computed({
  get: () => book.value.title,
  set: title => book.value.title = title,
})

const addNumber = () => array.value.push(array.value.length + 1)
</script>

<template>
  <div>
    <h2>{{ bookTitle }}</h2>
    <input v-model="bookTitle">
    <hr>
    <p>{{ plusOneNumber }}</p>
    <p>{{ evenNumbers }}</p>
    <button @click="addNumber">
      Add Number
    </button>
  </div>
</template>

watch()

  • watch():侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
  • watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
  • 第一个参数是侦听器的。这个来源可以是以下几种:
    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组
  • 第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
  • 当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
  • 第三个可选的参数是一个对象,支持以下这些选项:
    • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
    • flush:调整回调函数的刷新时机。参考回调的刷新时机watchEffect()
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
    • once: 回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
vue
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
watch(count, (count, prevCount) => {
  console.log(`Count changed: ${prevCount} -> ${count}`)
})

function autoIncrement() {
  setInterval(() => {
    count.value++
  }, 800)
}
</script>

<template>
  <div>
    <h1>监视单个数据的变化</h1>
    <p>Count: {{ count }}</p>
    <button @click="autoIncrement">
      Auto Increment
    </button>
  </div>
</template>
vue
<script setup>
import { ref, watch } from 'vue'

const book = ref({ title: 'Vue 3.0', author: 'Evan You', year: 2020 })
const name = ref('Zhangsan')
const age = ref(18)

watch([name, age], ([name, age], [prevName, prevAge]) => {
  console.log(`监视多个数据的变化:Name/Age changed: ${prevName}, ${prevAge} -> ${name}, ${age}`)
})

// 监视对象属性的变化
watch(() => [book.value.title, book.value.year], ([title, year], [prevTitle, prevYear]) => {
  console.log(`监视多个数据的变化:Book changed: ${prevTitle}, ${prevYear} -> ${title}, ${year}`)
})

function updateBookInfo() {
  book.value.title = 'Vue 3 指南'
  book.value.year = 2021
}

function updateUser() {
  name.value = 'Wangwu'
  age.value = 22
}
</script>

<template>
  <div>
    <h1>监视多个数据的变化</h1>
    <p>Book: {{ book.title }} - {{ book.year }}</p>
    <p>User: {{ name }} - Age: {{ age }}</p>
    <button @click="updateBookInfo">
      Update Book Info
    </button>
    <br>
    <button @click="updateUser">
      Update User
    </button>
  </div>
</template>
vue
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// immediate 立即执行回调函数
watch(count, (count, prevCount) => {
  console.log(`Count changed: ${prevCount} -> ${count}`)
}, { immediate: true })

function autoIncrement() {
  setInterval(() => {
    count.value++
  }, 800)
}
</script>

<template>
  <div>
    <h1>immediate 立即执行回调函数</h1>
    <p>Count: {{ count }}</p>
    <button @click="autoIncrement">
      Auto Increment
    </button>
  </div>
</template>
vue
<script setup>
import { ref, watch } from 'vue'

const book = ref({ title: 'Vue 3.0', author: 'Evan You', year: 2020 })

// 默认情况下,watch 监视的是数据的引用,如果数据的引用没有发生变化,watch 不会执行回调函数
watch(book, () => {
  console.log('Book changed:', book.value)
})

// deep 深度监视
watch(book, () => {
  console.log('Book changed:', book.value)
}, { deep: true })

function autoUpdateBookYear() {
  const timer = setInterval(() => {
    book.value.year++
    if (book.value.year === 2024)
      clearInterval(timer)
  }, 1000)
}
</script>

<template>
  <div>
    <h1>deep 深度监视</h1>
    <p>Book: {{ book.title }} - {{ book.year }}</p>
    <button @click="autoUpdateBookYear">
      Auto Update Book Year
    </button>
  </div>
</template>

生命周期钩子

选项式 API组合式 API
beforeCreate/createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
vue
<script setup>
// beforeCreate 和 created 的相关代码
// 一律放在 setup 中执行
import { onMounted } from 'vue'

function getList() {
  setTimeout(() => {
    [1, 2, 3, 4, 5].forEach((item) => {
      console.log(item)
    })
  }, 1000)
}

// 一进入页面的请求
getList()

// onMounted: 注册一个回调函数,在组件挂载完成后执行
// 如果有些代码需要在 mounted 生命周期中执行
// 写成函数的调用方式,可以调用多次,并不会冲突,而是按照顺序依次执行
onMounted(() => {
  console.log('mounted 生命周期函数 - 逻辑 1')
})

onMounted(() => {
  console.log('mounted 生命周期函数 - 逻辑 2')
})
</script>

<template>
  <div />
</template>

父子组件通信

一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。

props 父传子

  1. 父组件中给子组件绑定属性(通过 v-bind: 绑定),用于传递数据
  2. 子组件内部通过 defineProps 编译器宏生成 props 对象,用于接收父组件传递的数据
vue
<script setup>
import { ref } from 'vue'
import PoemContent from '@/PoemContent.vue'

const title = ref('燕歌行')
const content = ref([
  { id: 1, text: '孟冬初寒节气成,悲风入闺霜依庭。' },
  { id: 2, text: '秋蝉噪柳燕辞楹,念君行役怨边城。' },
  { id: 3, text: '君何崎岖久徂征,岂无膏沐感鹳鸣。' },
  { id: 4, text: '对君不乐泪沾缨,辟窗开幌弄秦筝。' },
  { id: 5, text: '调弦促柱多哀声,遥夜明月鉴帷屏。' },
  { id: 6, text: '谁知河汉浅且清,展转思服悲明星。' },
])
</script>

<template>
  <div>
    <h2>{{ title }}</h2>
    <!-- 父组件绑定 text 属性,向子组件传递数据 -->
    <PoemContent v-for="item in content" :key="item.id" :text="item.text" />
  </div>
</template>

<style lang="css" scoped>
div {
  width: 300px;
  background-color: #bbe2ec;
  text-align: center;
  margin: 20px auto;
  padding: 20px;
}
</style>
vue
<script setup>
// 子组件中使用 defineProps 编译器宏定义接收的 props
defineProps({ text: String })
</script>

<template>
  <li>{{ text }}</li>
</template>

<style lang="css" scoped>
li {
  list-style: none;
  margin: 10px 0;
}
</style>

$emit 子传父

  1. 子组件内通过 defineEmits 编译器宏生成 emit 方法。命名遵守驼峰规则 camelCase(enlargeText
  2. 子组件内部触发自定义事件并传递参数。
  3. 父组件中通过 v-on@ 绑定事件监听,用于接收子组件触发的事件。命名遵守短横线隔开式 kebab-case(enlarge-text)。

命名规则

遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。

vue
<script setup>
defineProps({ text: String, fontSize: String })
// 通过 defineEmits 定义一个名为 enlargeText 的事件,用于触发父组件中的事件监听
defineEmits(['enlargeText'])
</script>

<template>
  <li :style="{ listStyle: 'none', margin: '10px 0' }">
    {{ text }}
  </li>

  <!-- 通过 $emit 触发父组件中的事件监听,并传递参数 -->
  <button @click="$emit('enlargeText', `${parseFloat(fontSize) + 0.1}`)">
    Enlarge text
  </button>
</template>
vue
<script setup>
import { ref } from 'vue'
import PoemContent from '@/PoemContent.vue'

const title = ref('燕歌行')
const content = ref([
  { id: 1, text: '孟冬初寒节气成,悲风入闺霜依庭。' },
  { id: 2, text: '秋蝉噪柳燕辞楹,念君行役怨边城。' },
  { id: 3, text: '君何崎岖久徂征,岂无膏沐感鹳鸣。' },
  { id: 4, text: '对君不乐泪沾缨,辟窗开幌弄秦筝。' },
  { id: 5, text: '调弦促柱多哀声,遥夜明月鉴帷屏。' },
  { id: 6, text: '谁知河汉浅且清,展转思服悲明星。' },
])

const fontSize = ref('1.5')
</script>

<template>
  <div :style="{ fontSize: `${fontSize}em` }">
    <h2>{{ title }}</h2>
    <!-- 父组件中通过 @ 绑定事件监听,用于接收子组件触发的事件。命名遵守短横线隔开式 kebab-case -->
    <PoemContent
      v-for="item in content" :key="item.id" :font-size="fontSize" :text="item.text"
      @enlarge-text="fontSize = $event"
    />
  </div>
</template>

<style lang="css" scoped>
div {
  width: 800px;
  background-color: #bbe2ec;
  text-align: center;
  margin: 20px;
  padding: 20px;
}
</style>

模板引用

  • 概念:通过 ref 标识 获取真实的 dom 对象或者组件实例对象
  • 虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref<input ref="input">
  • ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

基本使用

  1. 声明一个 ref 来存放该元素的引用,必须和模板里的 ref 同名

    注意

    你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!

  2. 通过 ref 标识绑定 ref 对象到标签。

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const title = ref(null)
// 初次渲染前这个元素还不存在,所以这里的值是 null
console.log(`title: ${title.value}`)

// 也可以使用 ref 来存放 input 元素的引用
const input = ref(null)
// 初次渲染前这个元素还不存在,所以这里的值也是 null
console.log(`input: ${input.value}`)

// 页面加载后,自动聚焦 input 元素
onMounted(() => {
  input.value.focus()
})

function logRefs() {
  console.log(title.value, input.value)
}
</script>

<template>
  <h1 ref="title">
    Hello, Vue 3.0
  </h1>
  <input ref="input" placeholder="页面加载后,自动聚焦..." type="text">
  <hr>
  <button @click="logRefs">
    打印 ref
  </button>
</template>

defineExpose

defineExpose() | Vue.js

  • 使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
  • 可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性和方法。
vue
<script setup>
import { onMounted, ref } from 'vue'
import Child from './Child.vue'

const child = ref(null)
onMounted(() => {
  console.log(child.value)
  console.log(`child.name: ${child.value.name}`)
  console.log(`child.age: ${child.value.age}`)
  console.log(`child.changeAge: ${child.value.changeAge}`)
})
</script>

<template>
  <Child ref="child" />
</template>
vue
<script setup>
import { ref } from 'vue'

const name = ref('Zhang san')
const age = ref(20)

const changeAge = () => age.value += 1

// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({ name, age, changeAge })
</script>

<template>
  <div />
</template>

依赖注入

Prop 逐级透传问题

  • 通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props
  • 如果有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。如果组件链路非常长,可能会影响到更多这条路上的组件。这一问题被称为“prop 逐级透传”。
  • provideinject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者 Provide。任何后代的组件树,无论层级有多深,都可以注入 Inject由父组件提供给整条链路的依赖。

provide() 提供

  • 提供一个值,可以被后代组件注入。

  • 要为组件后代提供数据,需要使用到 provide() 函数。

    js
    provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
  • provide() 函数接收两个参数。

    • 第一个参数被称为注入名 key,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
    • 第二个参数是提供的值 value,值可以是任意类型,包括响应式的状态,比如一个 ref。
  • 提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。

  • 与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

  • 除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:

    jsx
    import { createApp } from 'vue'
    
    const app = createApp({})
    
    app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')

    在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。

inject() 注入

  • 注入一个由祖先组件或整个应用 (通过 app.provide()) 提供的值。

  • 要注入上层组件提供的数据,需使用 inject() 函数。

    js
    const message = inject('message')
  • 如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。

  • 第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。

  • 第二个参数是可选的,即在没有匹配到 key 时使用的默认值。

    第二个参数也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。在这种情况下,你必须将 true 作为第三个参数传入,表明这个函数将作为工厂函数使用,而非值本身。

  • 与注册生命周期钩子的 API 类似,inject() 必须在组件的 setup() 阶段同步调用。

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

import Child from './Child.vue'

// 跨层传递普通数据
const msg = 'Hello, Vite!'
provide('msg', msg)

// 跨层传递响应式数据
const count = ref(66)
provide('count', count)
const input = ref('我是 App.vue 中的 input 数据!')
provide('input', input)

// 跨层传递方法
function sayHello() {
  console.log('我是 App.vue 中的 sayHello 方法!')
}

provide('sayHello', sayHello)

// 自动增加 count
function autoIncrease() {
  const timer = setInterval(() => {
    count.value++
    if (count.value >= 100)
      clearInterval(timer)
  }, 500)
}
</script>

<template>
  <div :style="{ width: '600px', margin: '20px', padding: '20px', border: '1px solid #000', background: '#71C9CE' }">
    <h1>App.vue</h1>
    <p>跨层传递普通数据:{{ msg }}</p>
    <p>
      跨层传递响应式数据:{{ count }}
      <button @click="autoIncrease">
        Auto Increase
      </button>
    </p>
    <p>
      跨层传递方法:
      <button @click="sayHello">
        点击我
      </button>
    </p>
    <input v-model="input" :style="{ width: '300px', marginBottom: '20px' }">
    <br>
    <Child />
  </div>
</template>
vue
<script setup>
import GrandChild from './GrandChild.vue'
</script>

<template>
  <div :style="{ padding: '20px', border: '1px solid #000', background: '#A6E3E9' }">
    <h2>Child.vue</h2>
    <GrandChild />
  </div>
</template>
vue
<script setup>
import { inject } from 'vue'

// 跨层接收普通数据
const msg = inject('msg')

// 跨层接收响应式数据
const count = inject('count')
const input = inject('input')

// 跨层接收方法
const sayHello = inject('sayHello')
</script>

<template>
  <div :style="{ padding: '20px', border: '1px solid #000', background: '#CBF1F5' }">
    <h3>GrandChild.vue</h3>
    <p>跨层接收普通数据:{{ msg }}</p>
    <p>跨层接收响应式数据:{{ count }}</p>
    <p>
      跨层接收方法:
      <button @click="sayHello">
        点击我
      </button>
    </p>
    <p>跨层接收 input 数据:{{ input }}</p>
  </div>
</template>

Vue3.3 新特性

defineOptions

  • <script setup> 之前,如果要定义 props, emits 可以轻而易举地添加一个与 setup 平级的属性。

  • 但是用了 <script setup> 后,就没法这么干了 setup 属性已经没有了,自然无法添加与其平级的属性。

  • 为了解决这一问题,引入了 definePropsdefineEmits 这两个宏。但这只解决了 propsemits 这两个属性。

  • 如果我们要定义组件的 name 或其他自定义的属性,还是得回到最原始的用法——再添加一个普通的 <script> 标签。这样就会存在两个 <script> 标签。让人无法接受。

  • 所以在 Vue 3.3 中新引入了 defineOptions 宏。这个宏可以用来直接在 <script setup> 中声明组件选项,而不必使用单独的 <script> 块。这是一个宏定义,选项将会被提升到模块作用域中,无法访问 <script setup> 中不是字面常数的局部变量。

  • 可以用 defineOptions 定义任意的选项, props, emits, expose, slots 除外(因为这些可以使用 defineXxx 来做到)

vue
<script setup>
  defineOptions({
    name: 'App',
    inheritAttrs: false,
    // 更多自定义属性...
  })
</script>

defineModel()

  • 在 Vue3 中,自定义组件上使用v-model, 相当于传递一个modelValue属性,同时触发 update:modelValue 事件

    html
    <Child v-model="isVisible"></Child>
    // 相当于
    <Child :modelValue="isVisible" @update:modelValue="isVisible=$event"></Child>
  • 我们需要先定义 props,再定义 emits 。其中有许多重复的代码。如果需要修改此值,还需要手动调用 emit 函数。于是乎 defineModel 诞生了。

  • defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:

    • 它的 .value 和父组件的 v-model 的值同步;
    • 当它被子组件变更了,会触发父组件绑定的值一起更新。
vue
<script setup>
import { ref } from 'vue'
import Child1 from './Child1.vue'
import Child2 from './Child2.vue'

const msg = ref('Hello World!')
</script>

<template>
  <div :style="{ width: '600px', margin: '20px', padding: '20px', border: '1px solid #000', background: '#71C9CE' }">
    <h1>App.vue</h1>
    <p>msg: {{ msg }}</p>
    <span>My input</span> <input v-model="msg" :style="{ marginBottom: '20px' }">
    <Child1 v-model="msg" />
    <Child2 v-model="msg" />
  </div>
</template>
vue
<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>

<template>
  <div :style="{ marginBottom: '20px', padding: '20px', border: '1px solid #000', background: '#CBF1F5' }">
    <h2>Child1.vue</h2>
    <p>App.vue v-model: <strong>{{ modelValue }}</strong></p>
    <span>My input</span> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
  </div>
</template>
vue
<script setup>
const model = defineModel()
</script>

<template>
  <div :style="{ padding: '20px', border: '1px solid #000', background: '#A6E3E9' }">
    <h2>Child2.vue</h2>
    <p>App.vue v-model: <strong>{{ model }}</strong></p>
    <span>My input</span> <input v-model="model">
  </div>
</template>