vue3学习小札之(一):基础

537 阅读8分钟

引子

写在开始的一些“废话”:
记录自己学习vue3的过程,内容偏理论,大多来自vue3官网,提取比较重要的知识点,并穿插一些自己的理解,如有错误之处,还希望各位大佬指出~

可以前往专栏阅读该系列其他文章:传送门

在正式开始以前,先引入几个 vue3 / vue 中比较基础的概念。

  1. 单文件组件
    启用了构建工具的 vue 项目中,特有的文件,文件名格式为:*.vue。英文 Single-File Components,缩写为 SFC
    vue 框架为我们做处理好了这类文件所对应构建工具的配置,让我们可以使用一种类似 HTML 格式的文件来书写 vue 组件。
  2. 选项式 API & 组合式 API
    组合式 API 算是 vue3 相比较于 vue2 ,最明显的变动。
    先看一下两种 API 在书写格式上的不同,(摘自官网)。
    选项式
<script>
// 书写格式几乎等同于 vue2
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>

组合式

<script setup>
// 可以使用导入的 API 函数来描述组件逻辑 不需要定义 data methods 这类的选项
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 通常会与 <script setup>(点击查看) 搭配使用

需要注意的是,组合式 API 并不仅仅可以在单文件组件中使用,
因为它是一系列 API 的集合,所以
也可以通过setup()选项结合选项式 API 一起使用,
或者,在单独的外部文件中,通过导入 API 来书写组合式函数,达到逻辑复用的效果,
这些在后续的文章中会介绍,这里不做过多的展开。
官网的组合式 API 常见问答(点击查看)章节,详细阐述了组合式 API 的优势、细节,感兴趣的小伙伴可以前往阅读。

总的来说,vue3 中当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件的方式。
所以该系列的文章记录的主要都是组合式 API 的学习。既然选择了 vue3,那就拥抱它全新的全家桶吧(在这给自己挖个坑,后续打算整理一些 vite 的学习笔记)。

接下来,正式开始学习 vue3。
本文会列出组合式 API 的使用方法,具体的细节会附上官方的 API 链接。

创建应用

首先,从构建一个 vue3 应用开始。
可以选择 vite 本地构建、CDN 引入的方式搭建 vue3 环境,进行实操学习:前往官网查看
这里就接触到了第一个组合式 API : createApp(点击查看)

// main.js
// 每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例
import { createApp } from 'vue';
import router from './router/index';
import store from './store/index';

// 从一个单文件组件中导入根组件
import App from './App.vue';

// const app = createApp({
//   /* 根组件 选项 */
// });
const app = createApp(App);

// 确保在挂载应用实例之前完成所有应用配置!!!!!!!!!!

// 应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项
// 例如定义一个应用级的错误处理器,它将捕获所有由子组件上抛而未被处理的错误
app.config.errorHandler = (err) => {
  /* 处理错误 */
  console.error(err);
};

// 应用实例还提供了一些方法来注册应用范围内可用的资源
// 例如注册一个组件 这使得 TodoDeleteButton 在应用的任何地方都是可用的
app.component('TodoDeleteButton', TodoDeleteButton)

app
  .use(router)
  .use(store)
  // 应用实例必须在调用了 .mount() 方法后才会渲染出来。
  // 该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串
  // 容器元素自己将不会被视为应用的一部分。
  // .mount() 方法应该始终在整个应用配置和资源注册完成后被调用。
  // 同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。
  .mount('#app');

另外,vue3 中通过 createApp API 可以在同一个页面中创建多个应用实例,每个应用实例都拥有自己的用于配置和全局资源的作用域。
这一点与 vue2 中通过 new Vue 的方式很大的不同就是,vue2 中的多个实例会共用相同的原型链,出现了“污染”的情况。

模板语法

熟悉 vue2 的小伙伴肯定不会陌生 vue 的模板语法。
vue 维护了一套自己定义了语法的 HTML 语法。并通过 vue 的编译包,转为 DOM 结构,被规范的浏览器解析。
这其中就涉及到组件实例的数据如何绑定并呈现到 DOM 上。
这一块儿内容,vue3 和 vue2 的区别并不大,仍然是:

文本插值。“Mustache”语法 (即双大括号)的方式
文本插值属于数据绑定,可以将数据插入为纯文本

属性绑定。v-bind指令的方式
属性绑定可以响应式地绑定一个 HTML attribute

并且可以绑定的值包括:常量、属性、变量、js表达式、函数调用。
v-bind指令有一个简化用法:

const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}

// 用不简写 不带参数的 v-bind 可以将一个包含多个 attribute 的 JavaScript 对象
// 全量绑定到单个元素上
// 后续的 透传 Attributes 章节会用到这种写法
<div v-bind="objectOfAttrs"></div>

指令 Directives

上面介绍到v-bind,这是一个 vue 内置的指令。
指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令
一个指令的完整写法主要包括如下:

指令名

v- 前缀后紧跟的名字。
除了 vue 内置的指令,我们还可以自定义一些指令,为其命名。后续的文章会介绍自定义指令具体的实现方法。

参数 Arguments

某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。

// 这里 href 就是一个参数
// 它告诉 `v-bind` 指令将表达式 `url` 的值绑定到元素的 `href` attribute 上
<a v-bind:href="url"> ... </a>
// : 是属性绑定的简写
<a :href="url"> ... </a>

// 这里的参数是要监听的事件名称:`click`
<a v-on:click="doSomething"> ... </a>
// @ 是事件绑定的简写
<a @click="doSomething"> ... </a>

动态参数

可以将一个 JavaScript 表达式包含在一对中括号中,作为指令的动态参数。
JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。

动态参数中表达式的应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
如果需要传入一个复杂的动态参数,推荐使用计算属性替换复杂的表达式

修饰符 Modifiers

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。

// `.prevent` 修饰符会告知 `v-on` 指令对触发的事件调用 `event.preventDefault()`
<form @submit.prevent="onSubmit">...</form>

后续的文章会详细介绍 vue 内置指令的各种修饰符。

指令的值

=号后面跟着的,自然就是传递给指令的值 value 了。

响应式基础

vue3 中引入了两个用来创建响应式数据的 API。这一点与 vue2 有很大不同。

vue2 中是通过选项式的方式,将定义在 data 中的数据进行响应式处理。核心是运用了Object.defineProperty方法。
简述 vue2 的响应式实现:
vue 实例初始化,将 data 中的属性通过Object.defineProperty方法进行属性拦截
拦截过程中,进行依赖收集,并创建了渲染 Watcher计算属性 Watcher侦听器 Watcher
这样,当某一个属性变动时,会通知跟其相关的所有Watcher进行更新操作。
其中细节,感兴趣的同学可以翻看 vue2 的源码。

说回 vue3 ,新引入的两个 API 分别是 reactive() 和 ref()

reactive()

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

reactive()可以创建响应式对象、数组
并且创建的对象代理,默认是深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
当然 vue3 也提供了创建一个浅层响应式对象的 API
响应式对象其实是 JavaScript Proxy,其行为表现与一般对象相似。这里就看出了 vue3 实现响应式的核心与 vue2 是不同的。

由于reactive()返回的是一个原始对象的 Proxy,所以它和原始对象是不相等的。只有代理对象是响应式的,更改原始对象不会触发更新
当出现嵌套对象时,依靠深层响应性,响应式对象内的嵌套对象依然是代理:

const proxy = reactive({})

const raw = {}
// 这里在给一个对象代理赋值时 其实 proxy.nested 赋值的是 raw 的代理
proxy.nested = raw

// 所以两者并不相同
console.log(proxy.nested === raw) // false

reactive() 的局限性

局限性一:
仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumber 和 boolean 这样的 原始类型 无效。

局限性二:
Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失。

let state = reactive({ count: 0 })

// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })

当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,会失去响应性

const state = reactive({ count: 0 })

// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++

// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)

重点来了:
上面失去响应式的原因是因为 state.count 满足了局限性一,如果是如下的情况,就不一样了:

const state = reactive({ count: { number: 0 } })

// n 是一个局部变量,同 state.count
// 不会失去响应性连接
let n = state.count
n.number++

let { count } = state
count.number++

callSomeFunction(state.count)

因为 state 是一个深层响应式,n === state.count为 true ,所以当 n 是一个对象代理时,响应式对象的引用相同,响应式没有丢失。

ref()

reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref
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 属性也是响应式的。
同时,当入参值为对象类型时,会用 reactive() 自动转换它的 .value,所以一个包含对象类型值的 ref 可以响应式地替换整个对象。
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性,本质其实类似于reactive()局限性章节里的特殊情况。ref() 让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。

ref的解包

ref解包,意思就是访问 ref 的值,不需要.value
仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。如果一个 ref 是文本插值(即一个 {{ }} 符号)计算的最终值,它也将被解包,哪怕不是顶层属性。
下面看几个例子:

const object = { foo: ref(1) }
// 此时的 object.foo 不是顶层属性
// 所以无法解包
// object.foo 是一个 ref 对象
{{ object.foo + 1 }} // 错误
{{ object.foo.value + 1 }} // 正确

// 通过解构 可以提升为顶层属性
const { foo } = object
{{ foo + 1 }} // 正确

// 当最终值是一个 ref 对象,不存在额外的运行时,是可以解包的
// 这就是插值运算符的一个便利之处
{{ object.foo }}

当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它自动解包。
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。

计算属性

计算属性 API: computed()可以用来描述依赖响应式状态的复杂逻辑
computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似。其响应式表现和 vue2 相同,也会基于其响应式依赖被缓存
只传入 getter 函数的计算属性返回一个只读的响应式 ref 对象
可以通过同时提供 getter 和 setter 来创建一个“可写”的计算属性。并不推荐,计算属性最好是只读的,没有副作用的。

<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>

类与样式的绑定

在给 DOM 元素进行属性绑定时,指令最终的值一般都是字符串,但是由于 class 和 style 的特殊性,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。
因此,Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 HTML class

绑定对象:

// 动态切换 class
const isActive = ref(true)
const hasError = ref(false)
<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

// 直接绑定对象
const classObject = reactive({
  active: true,
  'text-danger': false
})
<div :class="classObject"></div>

// 绑定一个返回对象的计算属性
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
  active: isActive.value && !error.value,
  'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>

绑定数组:

const activeClass = ref('active')
const errorClass = ref('text-danger')
// 数组也可以嵌套对象
<div :class="[
      { 'active111': isActive },
      isActive ? activeClass : '',
      errorClass
    ]"></div>

当在组件上进行 class 绑定时:
对于只有一个根元素的组件,当你使用了 class attribute 时,这些 class 会被添加到组件的根元素上,并与该元素上已有的 class 合并
如果组件有多个根元素,需要指定哪个根元素来接收这个 class。可以通过组件的 $attrs 属性(透传 Attribute)来实现指定:

<MyComponent class="baz" />

// MyComponent 模板使用 $attrs
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>

绑定内联样式 style

同样支持对象、数组、计算属性的方式。

条件渲染

即内置指令:
v-if v-else-if v-else v-show
与 vue2 中的用法保持一致。
需要注意的是,当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行,优先级更高,所以v-if的条件中无法访问到v-for作用域内的变量,应尽量避免同时使用。
如果场景实在需要,可以如下处理:
外部包装一层 < template > 再在其上使用 v-for ,内部通过 v-if 判断;
或者,将判断逻辑放入计算属性,返回一个数组用于v-for

列表渲染

当需要基于一个 数组 / 对象 / 整数值,渲染一个列表时,就可以用到上一章节讲到的v-for内置指令。
用法与 vue2 一致。

通过 key 管理状态

v-for指令的重点在于需要为每个元素对应的块提供一个唯一的 key attribute
key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。

<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

这是因为 vue 的渲染机制导致的优化必需操作。
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

举个例子,
把数据的某两个元素互换位置,由于diff算法中“就地复用”的更新策略,没有变动的元素会直接复用,
但是互换位置的两个元素相对于原本位置的旧状态而言,是两个新的元素,所以会删除并重新创建。
然而用户其实只是进行了位置互换的操作,改变的只是索引,并没有内容上的更新。
所以v-for指令,就引入了key的概念,加上唯一的key之后,再进行上述的位置互换操作,diff算法就会根据key判断元素的新旧状态是否一致,如果一致,只是索引产生了变化,就会采用“复用”,从而实现了性能的优化。

事件处理

使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName" 或 @click="handler"
规则与 vue2 一致。

表单输入绑定

这一章涉及的就是 vue 的内置指令 —— v-model,即,数据双向绑定
这一块儿还是很重要的。

<input v-model="text">
// v-model 其实就是下面写法的语法糖形式
<input
  :value="text"
  @input="event => text = event.target.value">

针对于原生 DOM 元素,主要有以下几种:
文本类型的 <input> 和 <textarea> 元素会绑定 value property 并侦听 input 事件;
<input type="checkbox"> 和 <input type="radio"> 会绑定 checked property 并侦听 change 事件;
<select> 会绑定 value property 并侦听 change 事件。

v-model也可以使用在我们自己构建的具有自定义行为的可复用输入组件上。
这种情况下,我们除了可以监听默认的value属性和input等事件外,还可以通过给v-mode传递参数自定义需要监听的属性名事件名。这些内容在后续深入 vue 组件的文章里会详细介绍。

生命周期钩子

如果采用选项式 API 写法,那生命周期钩子函数与 vue2 一致。
但是如果采用的是组合式 API 的写法,vue3 的生命周期钩子 API 就不同了。
具体参考官方的 API 文档

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

onMounted(() => {
  console.log(`the component is now mounted.`)
})
</script>

侦听器

在计算属性章节中,我们提到过不建议在计算属性只中产生副作用、进行 DOM 操作等等。。。
但是如果我们需要更改 DOM,或是根据异步操作的结果去修改另一处的状态,那就需要用到侦听器:
使用 watch 函数在每次响应式状态发生变化时触发回调函数。

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY], [oldX, oldY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // 在嵌套的属性变更时触发
  // 注意:`newValue` 此处和 `oldValue` 是相等的
  // 因为它们是同一个对象!即,代理对象 obj
})

obj.count++

但是如果这个响应式对象并不是直接传入,而是通过一个 getter 函数传入的时候,并不会形成一个深层侦听,而是必须外部替换掉整个的响应式对象时,才能侦听到变化,触发回调。
对于这种情况,我们可以传入watch()的第三个参数,它是一个选项对象:

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // 注意:`newValue` 此处和 `oldValue` 是相等的
    // *除非* state.someObject 被整个替换了 才不相同
  },
  // 这样 就形成了深层侦听
  { deep: true }
)

类似于 vue2 中的immediate: true,vue3 中也提供了立即执行的侦听器: watchEffect 函数
同样的,出于性能考虑,vue3 也存在异步更新的机制。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
如果想在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项:

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

// 后置刷新的 `watchEffect()` 有个更方便的别名 `watchPostEffect()`
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* 在 Vue 更新后执行 */
})

一般情况下,同步语句创建的侦听器都会绑定在当前组件实例上,当组件卸载时,vue 会帮我们停止对应作用域下的侦听器。
但是我们也可以手动停止侦听器:调用 watch 或 watchEffect 返回的函数。

模板引用

为了能够让我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用,直接访问底层 DOM 元素,vue3 定义了一种特殊的ref attribute,类似于 vue2 中的 $refs

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

// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)

onMounted(() => {
  input.value.focus()
})
</script>

<template>
  <input ref="input" />
</template>

函数模板引用

除了上述,通过字符串名称,元素的ref属性也可以绑定为一个函数。
该函数会在每次组件更新时都被调用。并收到元素引用作为其第一个参数:

<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">

组件上的 ref

模板引用同样可以作用于组件,获得的值的是组件实例。

需要注意的是:
如果子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。
所以,这就带来了一个问题:
父组件和子组件会变得紧密耦合。开发者如果过于随意得使用这种方式,会导致后期维护变得困难。
大多数情况下,应该首先使用标准的 propsemit 接口来实现父子组件交互。

但是!!!
使用了 <script setup> 的组件是默认私有的:
一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

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

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

组件基础

接下来,就是 vue 或者说是现在前端模块化开发的重头戏 —— 组件。
组件可以独立的、可重用的。所以在日常开发中,组件的使用就显得很频繁、很重要。
这里,先简单得针对 vue3 中的组件做一个基础介绍,后续会有专门的文章来深入组件。

传递 props

Props 是一种特别的 attributes,你可以在组件上声明注册。这里要用到 defineProps 宏:

<!-- BlogPost.vue 组件 -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

当一个 prop 被注册后,可以在使用组件的父组件中,像这样以自定义 attribute 的形式传递数据给它:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

监听事件

子组件
可以通过调用内置的  $emit 方法,通过传入事件名称来抛出一个事件:

<!-- BlogPost.vue, 省略了 <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

也可以通过 defineEmits 宏来声明需要抛出的事件:
它返回一个等同于 $emit 方法的 emit 函数

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
// 声明需要抛出的事件
const emit = defineEmits(['enlarge-text'])
// 抛出事件
emit('enlarge-text')
</script>

父组件
可以通过 v-on 或 @ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

通过插槽来分配内容

组件为我们提供了封装、复用的能力,但是某些场景下,我们想要从父组件向子组件传递一些“定制化”的内容,这时候插槽(Vue 的自定义 <slot> 元素)就登场了。

// 父组件
<AlertBox>
  something bad happened.
</AlertBox>

// 子组件 <AlertBox>
// 使用 `<slot>` 作为一个占位符,父组件传递进来的内容就会渲染其对应的位置,并替换掉`<slot>`
<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

动态组件

有些场景会需要在两个组件间来回切换,比如 Tab 界面:

<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

传给 :is 的值可以是以下几种:
被注册的组件名
导入的组件对象

总结

本篇文章主要记录的是 vue3 的一些基础
其中比较重点的是响应式、计算属性、侦听器、模板引用、组件等内容。
相较于 vue2,由于 vue3 的组合式 API 的引入,导致这些内容都有了较大的不同,后续会深入介绍具体的细节。