💡简介

来自官网 https://cn.vuejs.org

1. 注册

在使用一个 Vue 组件之前,要先注册。

1.1 全局注册

在全局注册的组件,可以在此应用的任意组件的模板中使用。

使用应用实例的 app.component() 方法:

app
  .component("ComponentA", ComponentA)
  .component("ComponentB", ComponentB)
  .component("ComponentC", ComponentC);

全局注册的问题:

  • 在打包的时候不能被 tree-shaking

  • 在大型项目中让项目的依赖关系变得不那么明确

  • 可能会影响项目的后续维护,类似使用过多的全局变量一样

1.2 局部注册

在使用它的父组件中显式导入,只能在父组件中使用,不能在后代组件中使用。

  1. 在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册

  2. 否则就需要使用 components 选项来显式注册

<script setup>
  // 导入后可直接使用,不用注册
  import ComponentA from "./ComponentA.vue";
</script>

<template>
  <ComponentA />
</template>
import ComponentA from "./ComponentA.js"  // 导入

export default {
  components: {  // 显式注册
    ComponentA,
    ComponentA: ComponentA // 写法等价
  },
  setup() {
    // ...
  }
};

局部注册的优点:

  • 使组件之间的依赖关系更加明确

  • 并且对 tree-shaking 更加友好

1.3 组件名

建议使用 PascalCase(驼峰式)<PascalCase />

  • 可在模板中区分原生 HTML 元素和 web components(单文件组件和内联字符串模板)

  • DOM 模板中 PascalCase 标签名是不可用的

2. Props 声明

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute。

2.1 方式

  1. 使用 defineProps()

  2. 使用 props 选项

这声明方式的背后使用的都是 prop 选项,它们都分别支持数组格式和对象格式。比如:

<!-- 比如:defineProps() 和数组格式 -->
<script setup>
  const props = defineProps(["foo"]);

  console.log(props.foo);
</script>
// 比如:props 选项和对象格式
export default {
  props: {
    title: String,
    likes: Number
  },
  setup(props) {
    // setup() 接收 props 作为第一个参数
    console.log(props.title);
  }
};

其中,对象格式的 prop,包括了名称+预期类型的构造函数(方便 prop 校验)。构造函数可以是原生构造函数,也可以是自定义的类或构造函数。

2.2 名字和值

  1. prop 的名字

    • camelCase 式(DOM 模板除外)的好处(但优势不明显)

      • 是合法的 JavaScript 标识符,可以直接在模板的表达式中用

      • 可以避免在作为属性 key 名时必须加引号

    • 推荐用 kebab-case 形式,可以和 HTML attribute 对齐

      • <BlogPost title="My journey with Vue" /> 与默认 attribute 对齐了更舒服些

  2. prop 的值:任何类型的值

2.3 单向数据流

props 只能从父到子组件,而不会逆向传递。

如果想要更改一个 prop,可以用以下方式:

  1. prop 用来接收初始值,子组件再定义个局部数据属性

  2. 基于 prop 的计算属性

  3. 子组件抛出个事件来通知父组件更改数据(尤其是对引用型的 prop)

3. 事件

3.1 触发和监听

  • 触发和监听事件:在 v-on(简写为@)上

    1. 子组件:触发自定义事件,通过 $emit() 方法

    2. 父组件:监听事件

  • 关于组件自己触发的事件的说明

    1. 格式支持自动转换,用 camelCase 触发,用 kebab-case 来监听(同组件的 prop)

    2. 支持事件修饰符、可以带参数

    3. 重名:如果和原生事件重名,则监听器只会监听组件触发的那个,而不再响应原生事件

    4. 没有冒泡:只能监听直接子组件触发的事件

      • 如果要在平级组件或是跨越多层嵌套的组件间通信,可以

        1. 使用一个外部的事件总线

<!-- 子组件触发 -->
<button @click="$emit('someEvent')">click me</button>
<button @click="$emit('increaseBy', 1)">+1</button>

<!-- 父组件监听 -->
<MyComponent @some-event="callback" />
<MyButton @increase-by="(n) => count += n" />
<MyButton @increase-by="increaseCount" />

3.2 声明要触发的事件

推荐总是显式地声明组件要触发的事件:

  • 若是 <script setup> 则用 defineEmits()

  • 若是 setup() 则用 emits 选项

  • 同样,支持数组、对象(对触发事件的参数进行验证)

<script setup>
  const emit = defineEmits(["inFocus", "submit"]);

  function buttonClick() {
    emit("submit");
  }
</script>
export default {
  emits: ["inFocus", "submit"],
  setup(props, ctx) {
    ctx.emit("submit");
  }
};

// setup() 参数解构
export default {
  emits: ["inFocus", "submit"],
  setup(props, { emit }) {
    emit("submit");
  }
};

3.3 让组件支持 v-model

两种方法:

  1. 子组件内部要做的两件事

    1. 将内部原生 input 元素的 value attribute 绑定到 modelValue prop

    2. 输入新的值时在 input 元素上触发 update:modelValue 事件

  2. 子组件使用一个可写的,同时具有 getter 和 setter 的计算属性

    • get 方法需返回 modelValue prop

    • set 方法需触发相应的事件

<!-- 父组件 -->
<CustomInput v-model="message" />

eg. 方法一

<!-- 子组件 -->
<script setup>
  defineProps(["modelValue"]);
  defineEmits(["update:modelValue"]);
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

eg. 方法二

<!-- 子组件 -->
<script setup>
  import { computed } from "vue";

  const props = defineProps(["modelValue"]);
  const emit = defineEmits(["update:modelValue"]);

  const value = computed({
    get() {
      return props.modelValue;
    },
    set(value) {
      emit("update:modelValue", value);
    }
  });
</script>

<template>
  <input v-model="value" />
</template>

默认情况,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字,这样就能轻松实现多个 v-model 绑定了。

<CustomInput v-model:title="message" />

此外,我们还可以让一个自定义组件的 v-model 支持自定义的修饰符

4. 透传 attributes

  1. 自动透传 + 自动连续透传 + 自动合并

    • 不包括组件自己声明为 propsemits 的 attribute

    • 自动合并:从父元素上继承的和子元素根元素自己的

  2. 禁用 Attributes 继承:inheritAttrs: false

    • 适用场景:需应用在根节点以外的其他元素上

    • 访问方法:

      • 模板:可用 $attrs 访问到

      • <script setup> 中使用 useAttrs() API

      • setup() 函数中是上下文对象的一个属性 ctx.attrs

  3. 注意事项:

    • 透传 attributes 在 JavaScript 中保留了它们原始的大小写,不同于 props

    • v-on 事件 @click 在数据上会显示为 $attrs.onClick

    • 多个根节点的组件:没有自动 attribute 透传行为,所以需要我们显式绑定

    • attr 对象不是响应式的(考虑到性能因素)。若想要响应式,可用 prop 或 onUpdated()

<!-- 没有参数的 `v-bind` 会将一个对象的所有属性都作为 attribute 应用到目标元素上 -->
<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">click me</button>
</div>
<!-- 多根节点模板 -->
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

5. 插槽

插槽 <slot> 可以让组件接收模板内容。

  1. slot outlet,插槽出口,子组件里收

  2. slot content,插槽内容,父组件里传

插槽内容可以是任意合法的模板内容,比如文本、多个元素、其它组件等,插槽让组件更加灵活和更具有可复用性。

5.1 slot outlet 和 slot content

  1. 默认内容

  2. 具名插槽:静态名字 + 动态名字

    • 收:<slot name="header"></slot>

      • 插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽

    • 传:<template v-slot:header></template>,简写为 <template #header>

    • 传:<template #[dynamicSlotName]></template> 动态名字

  3. 其它情况

    • 没有提供 name 的 <slot> 出口会隐式地命名为 'default'

    • 所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容

5.2 插槽数据的作用域

可以访问到父组件的数据作用域,但无法访问子组件的数据。

Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的,也就是:父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

5.3 插槽数据也想访问子组件的

如果想要插槽的内容同时使用父组件域内和子组件域内的数据,可以在插槽出口(子组件)对插槽传递 props,详见作用域插槽(接收的参数只在该插槽作用域内有效)。

  1. 默认插槽传递和接收 props(下方示例)

  2. 具名插槽传递和接收 props

<slot :text="greetingMessage" :count="1"></slot>
```html
<!-- 默认插槽:接收子组件的 props -->
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

<!-- or 解构写法 -->
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>
```

6. 依赖注入

父组件给子组件传递数据,两种方式:

  1. props:当层次太深时不方便

  2. provideinject

    • 依赖提供者:父组件,也可以是整个应用实例层 app

    • 任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖

6.1 provide 提供

使用 provide() 函数,向后代组件提供数据。

  • 注入名:可以是字符串、Symbol(可以避免潜在的冲突)

  • 注入值:任意类型,包括响应式的状态

<script setup>
  import { provide } from "vue";

  // 可多次调用,以注册多个
  provide("message", "hello!"); // 注入名: 值
  provide("title", "Hi");
</script>
import { provide } from "vue";

export default {
  setup() {
    provide("message", "hello!"); // 在 setup() 同步调用的
  }
};
// 在应用层
import { createApp } from "vue";

const app = createApp({});

app.provide("message", "hello!");

6.2 inject 注入

使用 inject() 函数,来注入上层组件提供的数据:

<script setup>
  import { inject } from "vue";

  const message = inject("message");
</script>

7. 异步组件

  • 作用:仅在需要时再从服务器加载相关组件

  • 方法:defineAsyncComponent(),接收一个返回 Promise 的加载函数

eg1. ES 模块动态导入也会返回一个 Promise

import { defineAsyncComponent } from "vue";

// 仅在页面需要它渲染时才会调用加载内部实际组件的函数
const AsyncComp = defineAsyncComponent(() =>
  import("./components/MyComponent.vue")
);

eg2. 异步组件 + 内置的 <Suspense> 组件

8. 状态管理

每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了:

  1. 状态:驱动整个应用的数据源

  2. 视图:对状态的一种声明式映射

  3. 交互:状态根据用户在视图中的输入而作出相应变更的可能方式

然而,当有多个组件共享一个共同的状态时,就没有这么简单了,比如多个视图可能都依赖于同一份状态,比如来自不同视图的交互也可能需要更改同一份状态。

可选的办法:

  1. 将共享状态“提升”到共同的祖先组件上去,再通过 props 传递下来

    • 问题:prop 逐级透传问题,尤其是在深层次的组件树结构中的时候

  2. 抽取出组件间的共享状态,放在一个全局单例中来管理:用响应式 API 做简单状态管理,多个组件共用了单一的数据源 store 对象,代码示例如下。

    • 随之而来的问题:任意一个导入了 store 的组件都可以随意修改它的状态,不利于维护

    • 解决:在 store 上定义方法,方法名应该要能表达出行动的意图

    • 可作为 store 的:单个响应式对象, 其它响应式 APIref(), computed(), 通过一个组合式函数来返回一个全局状态

  3. 利用服务端渲染 (SSR) 的应用

  4. 官方的状态管理库 Pinia

    • Vue 之前的官方状态管理库是 Vuex,现在处于维护模式,但不再接受新的功能

    • 相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导

eg. 用响应式 API 做简单状态管理(手动管理)

// store.js
import { reactive } from "vue";

export const store = reactive({
  count: 0,
  increment() {
    this.count++;
  }
});
<!-- ComponentA.vue -->
<script setup>
  import { store } from "./store.js";
</script>

<template>From A: {{ store.count }}</template>
<!-- ComponentB.vue -->
<script setup>
  import { store } from "./store.js";
</script>

<template>From B: {{ store.count }}</template>

9. 更多

  1. 组件的封装原理

<!-- 组件的 key 属性 -->
<div v-for="child in children" :key="child.id" >{{ child.name }}</div>

Last updated