回顾 Vue 3 基础【Plan - May - Week 1】

发布于:2025-05-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

一、组件基础

1、定义组件

Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件(简称 SFC)

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

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

2、使用组件

在父组件中使用子组件,需要在父组件中导入它

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

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>
  • 通过 <script setup>,导入的组件都在模板中直接可用
  • 组件可以被重用任意多次
  • 每当使用一个组件时,就创建了一个新的实例,这个实例是单独的

3、传递 Props

父组件向子组件中传递数据,就要使用到 props

  • Props 是一种特别的 attributes,你可以在组件的 props 列表上声明,这里要用到 defineProps
  • defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props
<!-- 子组件 -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>
<!-- 父组件 -->
<script setup>
    ...
const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])
</script>

<template>
    <BlogPost
      v-for="post in posts"
      :key="post.id"
      :title="post.title"
    />
</template>

4、监听事件

子组件需要与父组件进行交互

  • 子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件
<!-- 子组件 -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

<template>
  <div>
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>
<!-- 父组件 -->
<script>
const postFontSize = ref(1)
</script>

<template>
	...
    <div :style="{ fontSize: postFontSize + 'em' }">
      <BlogPost
          ...
          @enlarge-text="postFontSize += 0.1"
       />
    </div>
</template>

5、通过插槽来分配内容

使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在该位置

<!-- 父组件 -->
<AlertBox>
  Something bad happened.
</AlertBox>
<!-- 子组件 -->
<!-- AlertBox.vue -->
<template>
  <div>
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

6、动态组件

有些场景会需要在两个组件间来回切换

<script setup>
import { ref } from 'vue'
 
const currentTab = ref('Home')

const tabs = {
  Home,
  Posts,
  Archive
}
</script>

<template>
  <div>
    <button
       v-for="(_, tab) in tabs"
       :key="tab"
     >
      {{ tab }}
    </button>
	  <component :is="tabs[currentTab]"></component>
  </div>
</template>
  • 传给 :is 的值可以是以下几种:

    • 被注册的组件名

    • 导入的组件对象

7、DOM 内模板解析

大小写区分

  • HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用 DOM 内的模板时,无论是 PascalCase 形式的组件名称、camelCase 形式的 prop 名称还是 v-on 的事件名称,都需要转换为相应等价的 kebab-case (短横线连字符) 形式
// JavaScript 中的 camelCase
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
<!-- HTML 中的 kebab-case -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

闭合标签

<MyComponent />
  • HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是 <input><img>。对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束
<my-component /> <!-- 我们想要在这里关闭标签... -->
<span>hello</span>
<!-- ------------------------------------------------ DOM 解析为:-->
<my-component>
  <span>hello</span>
</my-component> <!-- 但浏览器会在这里关闭标签 -->

二、模板语法

1、文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号)

<span>Message: {{ msg }}</span>
  • 双大括号标签会被替换为相应组件实例msg 属性的值。同时每次 msg 属性更改时它也会同步更新

2、原始 HTML

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令

3、Attribute 绑定

想要响应式地绑定一个 attribute,应该使用 v-bind 指令

<div v-bind:id="(status)"></div>
  • v-bind 指令指示 Vue 将元素的 id attribute 与组件的 status 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

简写

<div :id="status"></div>

同名简写

<div :id></div>
<!-- 等同 -->
<div v-bind:id></div>
  • 这个特性只在 Vue 3.4 及以上版本中可用

布尔型 Attribute

  • 布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上
<button :disabled="isButtonDisabled">Button</button>
  • isButtonDisabled真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略

动态绑定多个值

<script setup>
    ...
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper',
  style: 'background-color:green'
}
</script>

<template>
	...
	<div v-bind="objectOfAttrs"></div>
</template>

4、使用 JavaScript 表达式

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

仅支持表达式

  • 每个绑定仅支持单一表达式, 一个简单的判断方法是是否可以合法地写在 return 后面
<!-- 例如 -->
{{ var a = 1 }}

{{ if (ok) { return message } }}

5、指令 Directives

指令是带有 v- 前缀的特殊 attribute

参数 Arguments

  • 某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识
<a :href="url"> ... </a>
<a v-on:click="doSomething"> ... </a>
<!-- 简写 -->
<a @click="doSomething"> ... </a>

动态参数

  • 在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内
<a :[attributeName]="url"> ... </a>
<a @[eventName]="doSomething"> ... </a>
  • 这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数

动态参数值的限制

  • 动态参数中表达式的值应当是一个字符串,或者是 null

动态参数语法的限制

  • 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的
<!-- 例如 -->
<a :['foo' + bar]="value"> ... </a>
  • 需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写

修饰符 Modifiers

  • 修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定
<form @submit.prevent="onSubmit">...</form>
  • .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()

三、Props

1、Props 声明

props 可以使用 defineProps() 宏来声明

<script setup>
const props = defineProps(['foo'])

console.log(props.foo)
</script>
  • 除了使用字符串数组来声明 props 外,还可以使用对象的形式
<script setup>
defineProps({
  title: String,
  likes: Number
})
</script>

2、响应式 Props 解构

将解构的 props 传递到函数中

  • 将解构的 prop 包装在 getter 中来侦听解构的 prop
const { foo } = defineProps(['foo'])

watch(() => foo, /* ... */)
  • 当我们需要传递解构的 prop 到外部函数中并保持响应性时
useComposable(() => foo)

3、传递 prop 的细节

Prop 名字格式

  • 如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号
defineProps({
  greetingMessage: String
})

使用一个对象绑定多个 prop

  • 如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind,即只使用 v-bind 而非 :prop-name
<script setup>
const post = {
  id: 1,
  title: 'My Journey with Vue'
}
</script>

<template>
	<BlogPost v-bind="post" />
</template>
<!-- 等价于 -->
<template>
	<BlogPost :id="post.id" :title="post.title" />
</template>

4、单向数据流

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递

  • prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性
const props = defineProps(['initialCounter'])

// 计数器只是将 props.initialCounter 作为初始值
const counter = ref(props.initialCounter)
  • 需要对传入的 prop 值做进一步的转换
const props = defineProps(['size'])

// 该 prop 变更时计算属性也会自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())

5、Prop 校检

要声明对 props 的校验,可以向 defineProps() 宏提供一个带有 props 校验选项的对象

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // 必传但可为 null 的字符串
  propD: {
    type: [String, null],
    required: true
  },
  // Number 类型的默认值
  propE: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propF: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propG: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propH: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})
  • 所有 prop 默认都是可选的,除非声明了 required: true
  • Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

可为 null 的类型

  • 如果该类型是必传但可为 null 的,可以用一个包含 null 的数组语法
defineProps({
  id: {
    type: [String, null],
    required: true
  }
})

四、插槽 Slot

1、插槽内容与出口

<!-- 父组件 -->
<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
<!-- 子组件 -->
<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>
  • 插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件

2、默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>
  • 如果我们提供了插槽内容,那么被显式提供的内容会取代默认内容

3、具名插槽

<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
  • name 的插槽被称为具名插槽 (named slots)
  • 没有提供 name<slot> 出口会隐式地命名为“default”
  • 要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令
<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>
  • v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>
  • 当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- 隐式的默认插槽 -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

4、动态插槽名

动态指令参数v-slot 上也是有效的

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

5、作用域插槽

在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据,可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes

<!-- 子组件 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>
<!-- 父组件 -->
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
  • 同样可以在 v-slot 中使用解构

五、组合式 API - setup()

1、基本使用

我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例

<script>
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)

    return {
      count
    }
  },

  mounted() {
    console.log(this.count) // 0
  }
}
</script>

<template>
  <button @click="count++">{{ count }}</button>
</template>

2、访问 Props

如果需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 ==toRefs()==和 toRef() 这两个工具函数

import { toRefs, toRef } from 'vue'

export default {
  setup(props) {
    // 将 `props` 转为一个其中全是 ref 的对象,然后解构
    const { title } = toRefs(props)
    // `title` 是一个追踪着 `props.title` 的 ref
    console.log(title.value)

    // 或者,将 `props` 的单个属性转为一个 ref
    const title = toRef(props, 'title')
  }
}

3、Setup 上下文

上下文对象是非响应式的,可以安全地解构

export default {
  setup(props, { attrs, slots, emit, expose }) {
    ...
  }
}

暴露公共属性

expose 函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose 函数暴露出的内容

export default {
  setup(props, { expose }) {
    // 不向父组件暴露任何东西
    expose()

    const publicCount = ref(0)
    const privateCount = ref(0)
    // 有选择地暴露局部状态
    expose({ count: publicCount })
  }
}

4、与渲染函数一起使用

setup 也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return () => h('div', count.value)
  }
}
  • 返回一个渲染函数将会阻止我们返回其他东西
  • 我们可以通过调用 expose() 通过模板引用将这个组件的方法暴露给父组件
import { h, ref } from 'vue'

export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value

    expose({
      increment
    })

    return () => h('div', count.value)
  }
}

学习资料来源:

组件基础 | Vue.js
模板语法 | Vue.js
Props | Vue.js
插槽 Slots | Vue.js
组合式 API: setup() | Vue.js


网站公告

今日签到

点亮在社区的每一天
去签到