10分钟入门Vue3

前言:你的阅读速度够快,10 分钟能看完这篇文章。文章整体比较粗浅(入门级),如需深入了解细枝末节,请移步官网

1. Vue3Vue2 的区别

  1. 双向数据绑定原理:
    • Vue2使用的是Object.defineProperty()进行数据劫持,结合发布订阅的方式实现双向数据绑定。然而,这种方式存在一些缺点,如初始化时递归遍历对象会造成性能损失,新增或删除属性时需要使用Vue.set/delete这样的特殊API才能生效,对es6中的MapSet等数据结构不支持等。
    • Vue3则使用了Proxy代理,通过refreactive将数据转化为响应式数据。Proxy可以对整个对象进行监听,拦截对象的所有操作,包括新增、删除属性,以及数组内部数据的变化。此外,Proxy还可以省去for in、闭包等内容来提升效率,直接绑定整个对象即可。
  2. 组件模板:
    • Vue2要求组件模板必须有一个唯一的顶层元素包裹,如<template><div>...</div></template>
    • Vue3则支持碎片(Fragments),即组件可以拥有多个根节点,无需额外的包裹元素,如<template><div>...</div><p>...</p></template>
  3. API 类型:
    • Vue2使用选项式API(Options API),在代码中分割了不同的属性,如datacomputedmethods等。
    • Vue3引入了组合式API(Composition API),数据和方法都定义在setup函数中,并通过return统一返回。相比于旧的API使用属性来分组,组合式API使用方法来分割,使得代码更加简便和整洁。
  4. 生命周期:
    • Vue2的生命周期钩子包括beforeCreatecreatedbeforeMountmounted等。
    • Vue3的生命周期钩子在组合式API中有所变化,例如setup函数代替了beforeCreatecreated,其他生命周期钩子名也有所改变,但功能基本保持不变。
  5. 性能优化:
    • Vue3在性能上进行了多项优化,如Diff算法的提升,使得虚拟DOM的对比和更新更加高效。
  6. 其他新特性:
    • Vue3引入了Teleport组件,允许将组件的内容渲染到 DOM 树中的任何位置,而不仅仅是其子元素。
    • Vue3还支持Suspense组件,用于在异步组件加载时显示备用内容。
    • 还有其他更多新增的API请查看 API 参考

总结来说,Vue3Vue2的基础上进行了多方面的改进和优化,包括数据绑定原理、组件模板、API类型、生命周期、性能优化以及新特性的引入。这些改进使得Vue3在开发效率、代码简洁性和性能上都有了显著的提升。

Vue2Vue3就当成不同的但有点类似的框架去学习就好,两者更多不同请查看Vue 3 迁移指南,作者也说了:学习 Vue 3 的推荐方法是阅读新的文档

2. 基本语法

2.1 setup

  • setup 是什么?
    • setup 是一个特殊的组件选项,它在组件被创建之前被调用。
    • setup 函数中,你可以访问组件的 propscontext(如 attrsslotsemit 等)。
    • 你可以使用 Composition API 的函数(如 refreactivecomputed 等)来定义状态、计算属性和方法等。
    • 你返回的任何内容都可以在模板中直接使用(例如,通过 return 语句返回的状态和计算属性)。
    • setup中访问thisundefined
<!-- 2.1.1 -->
<template>
  <button @click="count++">{{ count }}</button>
</template>
<script lang="ts">
import { ref } from "vue";
export default {
  name: "Home",
  setup() {
    const count = ref(0);
    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count,
    };
  },
  mounted() {
    console.log(this.count); // 0
  },
};
</script>

2.2 setup 语法糖 <script setup>

2.1.1的例子可以使用语法糖改写成:

<!-- 2.2.1 -->
<template>
  <button @click="count++">{{ count }}</button>
</template>
<script setup lang="ts" name="Home">
import { ref, onMounted } from "vue";
const count = ref(0);
onMounted(() => {
  console.log(count.value); // 0
});
</script>

其中,当前组件名字 name="Home" 可以借助插件使其可以像2.2.1那样写,否则需要单独抛出<script lang="ts">export default {name:'Home'}</script>。插件使用如下:

  • 安装插件 npm i vite-plugin-vue-setup-extend -D
  • 在文件 vite.config.ts 中添加
// 2.2.2
import { defineConfig } from "vite";
import VueSetupExtend from "vite-plugin-vue-setup-extend";

export default defineConfig({
  plugins: [VueSetupExtend()],
});

2.3 基本类型的响应式数据 ref()

一般是定义一个响应式的基本类型,如 numberstringboolean 等。,在 setup 中使用要 .value 获取或者修改其值,在模板中可以直接使用。ref()也可以定义一个对象类型的响应式数据,但特别不建议

<template>
  {{ foo }}
</template>
<script setup lang="ts" name="Home">
import { ref } from "vue";

let foo = ref(0);
console.log(foo.value); // 0

foo.value = 9;
console.log(foo.value); // 9
</script>

2.4 对象类型的响应式数据 reactive()

定义一个响应式的对象类型。响应式是“深层”的:它会影响到所有嵌套的属性。

<script setup lang="ts" name="Home">
import { reactive } from "vue";

const obj = reactive({
  foo: 0,
  bar: {
    count: 1,
  },
});
console.log(obj.foo, obj.bar.count); // 0 1

obj.bar.count = 999;
console.log(obj.bar.count); // 999
</script>

下面的代码直觉上是没问题的,但有点背道而驰

<template>
  {{ obj.foo }}
  <button @click="change">btn</button>
</template>

<script setup lang="ts" name="Home">
import { reactive } from "vue";

let obj = reactive({
  foo: 0,
});

function change() {
  obj = reactive({
    foo: 100,
  });
}
</script>

我试图在点击按钮时通过调用 change 函数来更改 obj 对象的 foo 属性值。但是,方法中存在一个关键的问题:我正在重新为 obj 分配一个新的响应式对象,而不是修改现有的响应式对象。所以页面不会更新,显示还是 0

解决方法是直接修改该对象的属性来触发视图更新,而不是重新为变量分配一个新的响应式对象。

function change() {
  obj.foo = 100;
  // 直接修改源对象,这种写法也可以
  // Object.assign(obj, { foo: 100 });
}

2.5 toRef()toRefs()

toRef()可以基于响应式对象的一个属性,创建一个对应的ref

<template>
  {{ p.name }}
</template>

<script setup lang="ts" name="Home">
import { reactive, toRef } from "vue";

const p = reactive({
  name: "sasha",
  age: 18,
});

let name = p.name;
name = "lena";
</script>

上面的代码修改name = "lena",视图并不会更新,很明显这个不是一个响应式的值的修改

我们可以这样做,把最后两行代码修改为

let refName = toRef(p, "name");
refName.value = "lena";

这样就可以把响应式对象的一个属性单独拎出来变成一个响应式的值了

toRefs() 主要用于将 reactive 创建的响应式对象的属性转换为单独的响应式引用(ref)

<template>
  {{ p.name }}
  {{ p.age }}
</template>

<script setup lang="ts" name="Home">
import { reactive, toRefs } from "vue";

const p = reactive({
  name: "sasha",
  age: 18,
});

const refsP = toRefs(p);

// 视图更新为修改后的值
refsP.name.value = "lena";
refsP.age.value = 19;
</script>

2.6 计算属性 computed()

计算属性默认是只读的,当然也是可以修改的。接受一个 getter 函数,也就是可以是一个有返回值的函数或者带有 getset 函数的对象。

<!-- 函数写法 -->
<script setup lang="ts" name="Home">
import { ref, computed } from "vue";

const foo = ref(1);
const bar = ref(2);

const plus = computed(() => foo.value + bar.value);
console.log(plus.value); // 3
</script>
// 对象写法
const plus = computed({
  get: () => foo.value + bar.value,
  set(v) {
    // 做何修改在这里写
  },
});

2.7 侦听器 watch()

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

watch(source,callback,option)函数接受 3 个参数,下面简单解释下

1. source —— 侦听器的

  • 一个函数,返回一个值
  • 一个 ref
  • 一个响应式对象(或许是 reactive)
  • …或是由以上类型的值组成的数组

2. callback —— 参数在发生变化时要调用的回调函数

(newValue, oldValue, onCleanup) => {
  // newValue 新值
  // oldValue 旧值
  // onCleanup 副作用清理回调函数
};

3. option —— 一个对象,支持以下这些选项

  • immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。
  • flush:调整回调函数的刷新时机。
  • onTrack / onTrigger:调试侦听器的依赖。
  • once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。

一些注意点:

  1. 侦听 ref 定义的对象时,侦听的是对象的地址,要想侦听其属性,要加 deep
<script setup lang="ts">
import { ref, watch } from "vue";

const state = ref({
  foo: 1,
  bar: {
    name: "sasha",
  },
});

watch(
  state,
  () => {
    // do something
  },
  {
    deep: true,
  }
);
</script>
  1. 不能直接侦听响应式对象的属性值,例如:
<script setup lang="ts">
import { reactive, watch } from "vue";

const state = reactive({
  foo: 1,
});

// 错误的写法
watch(state.foo, () => console.log("state change"));
</script>

需要用一个返回该属性的 getter 函数

watch(
  () => state.foo,
  () => console.log("state change")
);
  1. 默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post'

2.8 侦听器 watchEffect()

watch 差不多,watchEffect 会自动跟踪回调的响应式依赖,不用特地指出侦听器的

watchEffect(callback,option) 只接收两个参数

callback 就是要运行的副作用函数 onCleanup,没有新值旧值
option 可以用来调整副作用的刷新时机flush或调试副作用的依赖onTrack / onTrigger

const state = reactive({
  foo: 1,
  bar: 2,
});

// 会自动跟踪 foo,bar
watchEffect(() => {
  if (state.foo > 1 || state.bar < 2) {
    // do something
  }
});

2.9 watchwatchEffect 的区别

Vue 3 中,watchwatchEffect 都是用于响应式地观察和响应数据变化的 API,但它们之间存在一些关键的区别。以下是它们之间的主要差异:

  1. 自动追踪依赖

    • watchEffect:会自动追踪其执行过程中依赖到的响应式数据,并在这些数据变化时重新执行。你不需要明确指定要观察哪些数据,Vue 会自动为你处理。
    • watch:需要明确指定要观察的数据源(可以是响应式引用、计算属性、函数返回值等),并在这些数据源变化时执行回调函数。
  2. 执行时机

    • watchEffect:在组件加载(setup 函数执行)时立即执行一次,然后在其依赖的数据变化时再次执行。
    • watch:不会在组件加载时立即执行回调函数,而是只在观察的数据源变化时执行。
  3. 停止观察

    • 两者都会返回一个停止观察的函数,你可以调用这个函数来停止观察。
    • Vue 组件中,当组件卸载时,Vue 会自动停止所有在该组件中创建的 watchwatchEffect 观察。
  4. 性能考虑

    • 由于 watchEffect 会自动追踪依赖,因此在某些情况下可能会导致不必要的重新执行,尤其是在复杂的大型应用中。
    • 使用 watch 可以更精确地控制观察哪些数据源,并在这些数据源变化时执行特定的逻辑,从而可能提高性能。
  5. 使用场景

    • watchEffect:适用于当你需要观察多个数据源并在它们中的任何一个变化时执行某些逻辑的情况。由于它会自动追踪依赖,因此你不需要明确指定要观察哪些数据源。
    • watch:适用于当你需要明确观察某个或某些数据源并在它们变化时执行特定逻辑的情况。你可以更精确地控制观察哪些数据源以及何时执行回调函数。
  6. 清除副作用

    • watchEffect 的回调函数中,你可以使用 onInvalidate 函数来注册一个清理函数,该函数会在 watchEffect 停止观察之前被调用。这可以用于清除定时器、取消网络请求等副作用。
    • watch 没有直接的 onInvalidate 功能,但你可以在回调函数中手动管理副作用的清除。

总的来说,watchwatchEffect 提供了不同的方式来观察和响应 Vue 组件中的数据变化。你应该根据你的具体需求来选择使用哪个 API

2.10 何时使用 watchwatchEffect

使用 watch 的情况:

  1. 明确知道要观察哪个数据:如果你明确知道需要观察哪个响应式引用或对象的属性,那么使用 watch 是合适的。你可以将需要观察的数据作为 watch 的第一个参数传入,并定义一个回调函数来处理数据变化。

  2. 需要访问旧值:在 watch 的回调函数中,你可以访问到数据变化前后的旧值和新值。这对于需要比较旧值和新值来执行某些操作的场景非常有用。

  3. 性能优化:由于 watch 只会在指定的数据源变化时执行回调函数,因此它通常比 watchEffect 更高效。如果你只需要观察少数几个数据源,并且不需要在每次组件渲染时都重新执行副作用函数,那么使用 watch 可以减少不必要的计算。

使用 watchEffect 的情况:

  1. 自动追踪依赖:如果你想要自动追踪某个副作用函数内部所依赖的响应式数据,并在这些数据变化时重新执行副作用函数,那么使用 watchEffect 是合适的。watchEffect 会自动收集副作用函数中所使用的响应式数据作为依赖,并在这些依赖变化时重新执行副作用函数。

  2. 无需明确指定数据源:如果你不确定需要观察哪些数据源,或者你的副作用函数依赖于多个数据源,并且这些数据源可能会在未来发生变化,那么使用 watchEffect 可以更灵活地处理这种情况。

  3. 在组件加载时立即执行watchEffect 会在组件加载(即 setup 函数执行)时立即执行一次副作用函数,并在依赖的数据变化时再次执行。这对于需要在组件加载时立即执行某些初始化逻辑的场景非常有用。

总结:

  • 如果你明确知道要观察哪个数据源,并且需要访问旧值或进行性能优化,那么使用 watch
  • 如果你想要自动追踪副作用函数中的依赖,并在依赖变化时重新执行副作用函数,或者你不确定需要观察哪些数据源,并且希望在组件加载时立即执行某些逻辑,那么使用 watchEffect

2.11 模板引用 ref

用于普通 DOM 元素,引用元素本身

<template>
  <p ref="p">hello</p>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";

const p = ref();

// 因为 ref 本身是作为渲染函数的结果来创建的,必须等待组件挂载后才能对它进行访问。
onMounted(() => console.log(p.value)); // <p>hello</p>
</script>

用于子组件,引用将是子组件的实例

<template>
  <Child ref="c" />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
import { ref, onMounted } from "vue";

const c = ref();

onMounted(() => console.log(c.value));
</script>

2.12 props 写法

props 是子组件从父组件接收到的数据,子组件不能修改 props 的值。本节主要介绍 props 的三种写法。

<!-- 子组件 -->
<script setup lang="ts">
// 1. 简单接收
const props = defineProps(["foo"]);
// 或者 限制类型
// const props = defineProps({
//   foo: String,
// });
console.log(props.foo);

// 2. 通过泛型参数来定义 props 的类型
const props = defineProps<{
  foo: string;
  bar?: number;
}>();

// 当使用基于类型的声明(泛型参数)时,
// 我们失去了为 props 声明默认值的能力。

// 3. 可以通过 withDefaults 编译器宏解决

export interface Props {
  msg?: string;
  labels?: string[];
}

const props = withDefaults(defineProps<Props>(), {
  msg: "hello",
  labels: () => ["one", "two"],
});
</script>

defineProps 是只能在 <script setup> 中使用的编译器宏,不需要导入。还有其他一些宏,这里不展示。

2.13 生命周期

下面是一张 选项式API(Options API) 的生命周期图

请添加图片描述

vue3 推荐使用 组合式API(Composition API)生命周期函数setup替代了 beforeCreatecreated

  1. onMounted: 相当于 Options API 中的 mounted 钩子。

  2. onUpdated: 相当于 Options API 中的 updated 钩子。

  3. onUnmounted: 相当于 Options API 中的 unmounted 钩子。

  4. onBeforeMount: 相当于 Options API 中的 beforeMount 钩子。

  5. onBeforeUpdate: 相当于 Options API 中的 beforeUpdate 钩子。

  6. onBeforeUnmount: 相当于 Options API 中的 beforeUnmount 钩子。

使用都大同小异,以 onMounted为例:

// onMounted 类型
function onMounted(callback: () => void): void
<!-- 使用 -->
<script setup>
import { onMounted } from "vue";

onMounted(() => {
  // do something
});
</script>

2.14 自定义 hook

规则太多则过于死板,无限制的开放形同裸奔。vue 在权衡利弊后,最终,官网文档并没有 hook 😂。

不过有类似的东西,使用起来没有那么复杂。Composition API 允许你将组件的逻辑分割成可重用的函数,这些函数被称为 composable functions(可组合函数) 或 composables

Vue 3 中,Composition API 的主要组成部分包括:

  • refreactive:用于创建响应式引用和对象。
  • computed:用于创建计算属性。
  • watchwatchEffect:用于侦听响应式数据的变化并执行副作用。
  • provideinject:用于依赖注入。

现在我要实现一个简单的计算加减功能

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Is Zero: {{ isZero }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(0);

const increment = () => count.value++; // 加 1
const decrement = () => count.value--; // 减 1

// 创建一个计算属性,判断是否为零
const isZero = computed(() => count.value === 0);
</script>

这个功能使用到了 refcomputed ,我们可以自定义 hook

封装计数器的逻辑

// useCounter.ts
import { ref, computed } from "vue";

// 自定义 hook: useCounter
export default function useCounter() {
  const count = ref(0);

  const increment = () => count.value++;
  const decrement = () => count.value--;

  const isZero = computed(() => count.value === 0);

  return {
    count,
    increment,
    decrement,
    isZero,
  };
}

使用

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Is Zero: {{ isZero }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script setup lang="ts">
import useCounter from "@/hooks/useCounter";
const { count, isZero, increment, decrement } = useCounter();
</script>

这样,组件部分看起来就简洁了许多,且逻辑部分可以复用。对比 React HooksVue3为什么要这么设计?请看官方文档 和 React Hooks 的对比

3. 路由基础

3.1 创建和注册路由

创建路由器实例

// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";

import HomeView from "@/views/HomeView.vue";
import AboutView from "@/views/AboutView.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/about",
      component: AboutView,
    },
  ],
});

export default router;

假如现在有 2 个路由组件(通常存放在pagesviews文件夹)HomeViewAboutView

createWebHistory 表示基于 HTML5 history API 创建的路由实例,无特殊字符,需要后端配置的 URL。入参默认为空,import.meta.env.BASE_URL这个变量是 ViteVue CLI 3+ 等现代前端构建工具所提供的,用于支持环境变量和公共 URL 的注入,通常用于指定应用的基础 URL

注册路由器插件

// main.ts
import { createApp } from "vue";

import App from "./App.vue";
import router from "./router";

const app = createApp(App);
app.use(router); // 注册
app.mount("#app");

3.2 RouterLinkRouterView

RouterLink 用于实现路由跳转,RouterView 用于动态显示根据当前路由匹配到的组件内容。两者协同工作。

组件 RouterLinkRouterView 都是全局注册的,因此它们不需要在组件模板中导入。

<!-- src/app.vue -->
<template>
  <h1>Hello App!</h1>
  <p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
  <nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <RouterLink to="/about">Go to About</RouterLink>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
// 也可以通过局部导入它们,其实没必要写
import { RouterLink, RouterView } from "vue-router";
</script>

上述示例还使用了 {{ $route.fullPath }} 。你可以在组件模板中使用 $route 来访问当前的路由对象。

上面的例子中,切换 2 个页面时,另一个页面默认是被卸载掉的,显示当前页面时挂载

3.3 不同的历史模式

Hash 模式

createWebHashHistoryURL 带有一个一个哈希字符(#),不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。如果你担心这个问题,可以使用 HTML5 模式。

import { createRouter, createWebHashHistory } from "vue-router";

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    //...
  ],
});

HTML5 模式

createWebHistoryURL很漂亮,不带#,需要在服务器上添加一个简单的回退路由,否则可能会得到一个 404 错误。

import { createRouter, createWebHistory } from "vue-router";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    //...
  ],
});

Memory 模式

createMemoryHistoryNode 环境或者 SSR (服务器端渲染)使用,不解释

3.4 路由跳转时 to 的两种写法

就是在模板中使用编程式导航

<RouterLink to="/">Go to Home</RouterLink>
<RouterLink :to="{ path: '/about' }">Go to About</RouterLink>

3.5 命名路由

创建路由的时候提供 name,最大的受益就是传参,传参在后面讲。

注意: 所有路由的命名都必须是唯一的。如果为多条路由添加相同的命名,路由器只会保留最后那一条。

// 参照3.1的例子修改
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/about",
      name: "about", //新增 name
      component: AboutView,
    },
  ],
});

使用

<RouterLink :to="{ name: 'about' }">Go to About</RouterLink>

这跟代码调用 router.push() 是一回事:

<template>
  <nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <a @click="toAbout">Go to About</a>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
import router from "./router";

function toAbout() {
  router.push({ name: "about" });
}
</script>

3.6 嵌套路由

嵌套路由允许我们在一个 Vue 组件内部使用<router-view>标签来渲染另一个路由视图,即一个路由渲染的结果中包含另一个路由的渲染结果,形成嵌套关系。嵌套路由的关键配置包括:

  • <router-view> 标签:用于声明被嵌套组件的渲染位置。
  • 路由配置表:在路由配置中,使用 children:[] 来声明嵌套的子路由。
  • 子路由的 path 属性:子路由的 path 属性中不可以带/,否则无法匹配。
  • 无限嵌套:嵌套路由可以无限嵌套,形成多层次的页面结构。

来个最简单的例子,还是基于本章的路由来修改:

/about/foo                            /about/bar
+------------------+                  +-----------------+
| About            |                  | About           |
| +--------------+ |                  | +-------------+ |
| | Foo          | |  +------------>  | | Bar       | |
| |              | |                  | |             | |
| +--------------+ |                  | +-------------+ |
+------------------+                  +-----------------+
// router/index.js
import { createRouter, createWebHistory } from "vue-router";

import HomeView from "@/views/HomeView.vue";
import AboutView from "@/views/AboutView.vue";
// 新增两个子组件
import Foo from "@/components/Foo.vue";
import Bar from "@/components/Bar.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      name: "about",
      path: "/about",
      component: AboutView,
      children: [
        {
          // 当 /about/foo 匹配成功
          // Foo 将被渲染到 AboutView 的 <RouterView> 内部
          path: "foo",
          component: Foo,
        },
        {
          // 类似同上
          path: "bar",
          component: Bar,
        },
        // 可以添加一个重定向,当用户访问 /about 但没有指定子路由时,
        // 重定向规则会告诉 Vue Router 将用户导航到 /about/foo
        {
          name: "AboutRedirect", // 给这个重定向路由一个名字(不是必须的),不加会触发警告
          path: "",
          component: Foo,
        },
      ],
    },
  ],
});

export default router;
<!-- AboutView 路由组件 -->
<template>
  <h1>About</h1>
  <RouterLink to="/about/foo">to foo</RouterLink>
  <RouterLink to="/about/bar">to bar</RouterLink>

  <RouterView></RouterView>
</template>

当然,实际开发中可能没这么简单,嵌套的子路由可能需要数据展示,也许它不止两个,涉及到路由组件传参和渲染列表,这是后话了。

3.7 路由组件传参

3.7.1 通过路由的 query 传递参数

新建一个 User 路由组件

// src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";

import HomeView from "@/views/HomeView.vue";
import User from "@/views/User.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user",
      component: User,
    },
  ],
});

export default router;
<!-- App.vue -->
<template>
  <h1>Hello App!</h1>
  <p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
  <nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <!-- 在这里传入一个 id=123 的参数 -->
    <RouterLink to="/user?id=123">Go to User</RouterLink>
    <!-- 另一种写法 -->
    <RouterLink
      :to="{
        path: '/user',
        query: {
          id: 123,
        },
      }"
      >Go to User</RouterLink
    >

    <!-- 使用编程式导航 -->
    <a @click="toUser">Go to User</a>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
import router from "./router";

function toUser() {
  router.push({
    path: "/user",
    query: {
      id: 123,
    },
  });
}
</script>
<!-- User.vue -->
<!-- 可以在setup或模板中使用 -->
<template>
  <h1>User ID:{{ route.query.id }}</h1>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();
console.log(route.query); // {id: '123'}
</script>

使用 query 传参时,参数会作为 URL 的查询字符串,例如 /user?id=123,好处就是刷新页面时 query 传参的方式参数不会丢失。而 params 传参的方式可能会丢失参数(取决于服务器配置)。

query 不宜传入太多东西,URL 越长越容易引起人们的不适。

3.7.2 通过路由的 params 传递参数

User 组件上修改

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user/:id", // 注意这里的:id是动态参数
      name: "user",
      component: User,
    },
  ],
});
<template>
  <h1>Hello App!</h1>
  <p><strong>Current route path:</strong> {{ $route.fullPath }}</p>
  <nav>
    <RouterLink to="/">Go to Home</RouterLink>
    <!-- 这里的 3 种写法同 query -->
    <RouterLink to="/user/123">Go to User</RouterLink>
    <RouterLink
      :to="{
        name: 'user', // 使用 params 就不能使用 path 来跳转
        params: {
          id: 123,
        },
      }"
      >Go to User</RouterLink
    >
    <a @click="toUser">Go to User</a>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
import router from "./router";

function toUser() {
  router.push({
    name: "user",
    params: {
      id: 123,
    },
  });
}
</script>
<!-- User.vue -->
<template>
  <h1>User ID:{{ route.params.id }}</h1>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();
console.log(route.params); // {id: '123'}
</script>

3.7.3 paramsquery 的区别

  1. 引入方式

    • query:通常使用 path 来引入,即 URL 中直接拼接查询参数。
    • params:需要使用 name 来引入路由,如果尝试使用 path 引入并传递 params,则参数会被忽略,并可能出现警告。
  2. 参数位置

    • query:参数位于 URL 的查询字符串中,即在“?”之后,多个参数之间使用“&”分隔。
    • params:参数是路由的一部分,通常位于 URL 的路径中,以冒号(:)开始,用于标识相对应的值。
  3. 参数显示

    • query:参数会显示在浏览器的地址栏中,类似于 AJAX 中的 GET 请求。
    • params:参数不会显示在浏览器的地址栏中,类似于 POST 请求。
  4. 刷新影响

    • query:刷新页面后,通过 query 传递的参数仍然会保留在地址栏中,因此值不会丢失。
    • params:刷新页面后,通过 params 传递的参数可能会丢失,因为它们不是 URL 的一部分。
  5. 获取方式

    • query:可以通过this.$route.query来获取通过 query 传递的参数。
    • params:可以通过this.$route.params来获取通过 params 传递的参数。
  6. 使用场景

    • query:适用于那些对安全性要求不高,且需要在 URL 中直接展示参数的场景,如搜索查询等。
    • params:适用于那些需要隐藏参数或进行动态路由的场景,如用户详情页等。

3.7.4 路由的 props

Vue Router 允许我们为路由组件定义额外的 props,这样我们可以将除了 params 和 query 之外的任何数据传递给路由组件。

为什么需要路由的 props?

  • 灵活性:通过路由的 props,我们可以传递任何类型的数据给路由组件,而不仅仅是 URL 中的参数。这使得我们可以根据应用的需求来定制路由组件的数据源。

  • 解耦:在某些情况下,我们可能不希望路由组件直接依赖 URL 中的参数。使用路由的 props 可以将数据和 URL 解耦,使路由组件更加独立和可复用。

  • 自定义逻辑:路由的 props 允许我们定义自定义的逻辑来确定如何传递 props 给路由组件。例如,我们可以根据某些条件来决定是否传递某些 props,或者根据路由参数来动态计算 props 的值。

props 选项可以是一个布尔值、对象或函数。还是那个 User 组件

1. 布尔值

当 props 设置为 true 时,路由参数 params 将被设置为组件的 props

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user/:id",
      name: "user",
      component: User,
      props: true, // 将 (route.params) 设置为组件 props
    },
  ],
});
<!-- App.vue -->
<RouterLink to="/user/123">Go to User</RouterLink>
<!-- User.vue -->
<template>
  <h1>User ID:{{ id }}</h1>
</template>
<script setup lang="ts">
import { defineProps } from "vue";

const { id } = defineProps(["id"]);
console.log(id); // 123
</script>

2. 对象

如果 props 是一个对象,那么这个对象中的属性将直接作为 props 传递给路由组件,而与 query 和 params 无关。当 props 是静态的时候很有用。

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user",
      name: "user",
      component: User,
      props: {
        myName: "lena", // 注意这里并没有提到query或params
      },
    },
  ],
});
<!-- App.vue -->
<RouterLink to="/user">Go to User</RouterLink>
<!-- User.vue -->
<template>
  <h1>User Name:{{ myName }}</h1>
</template>
<script setup lang="ts">
import { defineProps } from "vue";

const { myName } = defineProps(["myName"]);
console.log(myName); // lena
</script>

props 选项设置为对象的情况下。query 和 params 仍然可以通过其他方式被访问和使用,但它们不会自动被设置为组件的 props。

当然,一般不会这么干,因为 props 设置为函数可以将静态值与基于路由的值相结合。

总得尝试,比如上面的例子混合 params 使用:

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user/:id", //加了id
      name: "user",
      component: User,
      props: {
        myName: "lena",
      },
    },
  ],
});
<!-- App.vue -->
<!-- 传入id -->
<RouterLink to="/user/123">Go to User</RouterLink>
<!-- User.vue -->
<template>
  <h1>User ID:{{ id }}</h1>
  <h1>User Name:{{ myName }}</h1>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
import { useRoute } from "vue-router";

// params 传入的 id 就不能使用 props 来访问,也没有 id 这个属性
const { id } = useRoute().params;
const { myName } = defineProps(["myName"]);
</script>

3. 函数

当 props 是一个函数时,这个函数将接收路由对象作为参数,并返回一个对象,该对象将被设置为组件的 props。

还是修改上面的例子,这里的 props 包括静态值、query 对象获取的值和一个逻辑判断动态设置的值。

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      component: HomeView,
    },
    {
      path: "/user",
      name: "user",
      component: User,
      props: (route) => ({
        // / 静态数据
        staticProp: "Lena Anderson",
        // 从query中获取数据
        searchQueryId: route.query.id || "",
        // 假设我们还有一个额外的逻辑,比如根据某个条件设置另一个 prop
        isAdvancedSearch: route.query.advanced === "true",
      }),
    },
  ],
});
<!-- App.vue -->
<RouterLink
  :to="{
    path: '/user',
    query: {
      id: 123,
      advanced: 'true',
    },
  }"
>Go to User</RouterLink>
<!-- User.vue -->
<template>
  <h1>staticProp:{{ staticProp }}</h1>
  <h1>User ID:{{ searchQueryId }}</h1>
  <h1>isAdvancedSearch:{{ isAdvancedSearch }}</h1>
</template>
<script setup lang="ts">
import { defineProps } from "vue";

const props = defineProps(["staticProp", "searchQueryId", "isAdvancedSearch"]);
console.log(props);
//{staticProp: 'Lena Anderson', searchQueryId: '123', isAdvancedSearch: true}
</script>

3.8 RouteLocationOptions

主要介绍 replace (boolean): 如果为 true,则导航不会向历史记录添加新条目,而是替换当前条目。这等价于直接调用 router.replace() 而不是 router.push()。

使用:

<!-- 3 种方式都可以 -->
<template>
  <nav>
    <RouterLink replace to="/">Go to Home</RouterLink>
    <RouterLink
      :to="{
        path: '/user',
        replace: true,
      }"
      >Go to User</RouterLink
    >
    <a @click="toUser">Go to User</a>
  </nav>
  <main>
    <RouterView />
  </main>
</template>

<script setup lang="ts">
import router from "./router";

function toUser() {
  router.replace({
    path: "/user",
    replace: true,
  });
}
</script>

3.9 useRoute 和 useRouter

useRouteuseRouter 是 Vue Router 提供的两个 Composition API Hooks,它们在 Vue 3 中与 Composition API 集成,使得在 Vue 组件中访问和操作路由变得更加便捷。

先看一个有趣的:

<script setup lang="ts">
import router from "./router"; // 上面很多例子都是这么写的
import { useRouter } from "vue-router";

const routerHook = useRouter();
console.log(router === routerHook); // true
</script>

router 是从 ./router 文件中导入的路由实例,而 routerHook 是通过 useRouter Hook 获取的路由实例。

因为 Vue Router 的实例通常被设计为单例,意味着在应用中只会创建一个路由实例,并且这个实例会在整个应用中被共享。因此,无论您是通过导入还是通过 useRouter Hook 获取,它们都应该引用相同的路由实例。

useRoute

  • 功能useRoute 是一个 Composition API Hook,用于在组件中获取当前路由的信息。
  • 返回值:它返回一个包含当前路由信息的对象,这个对象包含了路由的许多信息,如路径(path)、参数(params)、查询(query)、hash 等。
  • 适用场景:适用于那些不需要监听路由变化的场景,只是获取当前路由信息的静态数据。
  • 示例
<script setup lang="ts">
import { useRoute } from "vue-router";

const route = useRoute();
console.log(route.path); // 输出当前路由的路径
console.log(route.params); // 输出当前路由的参数
console.log(route.query); // 输出当前路由的查询参数
</script>

useRouter

  • 功能useRouter 也是一个 Composition API Hook,但它返回的是路由的实例,而不是当前路由的路由对象。
  • 返回值:这个路由实例包含了路由的许多方法,如导航(pushreplace)、编程式导航等。
  • 适用场景:适用于那些需要进行动态路由操作的场景,如编程式导航、监听路由变化等。
  • 示例
<script setup lang="ts">
import { useRouter } from "vue-router";

const router = useRouter();
router.push("/home"); // 导航到/home路径
router.replace("/about"); // 替换当前路由为/about路径
router.go(-1); // 后退一步
</script>

总结

  • useRoute 用于获取当前路由的信息,适用于静态场景。
  • useRouter 用于获取路由实例,可以执行路由的导航和编程式导航等操作,适用于动态场景。

3.10 重定向

redirect 是一个用于指定在访问某个路由路径时应该重定向到另一个路径的属性。这个属性可以在路由配置中设置,使得当用户尝试访问某个特定路径时,他们会被自动导航到另一个不同的路径。

const routes = [
  { path: "/home", component: HomeComponent },
  { path: "/", redirect: "/home" }, // 当访问根路径时,重定向到/home

  // 对象形式
  { path: "/", redirect: { name: "home" } },

  // 函数形式
  {
    path: "/",
    redirect: (to) => {
      // 假设isAuthenticated是一个函数,用于检查用户是否已认证
      if (isAuthenticated()) {
        return { name: "admin" }; // 如果已认证,重定向到admin页面
      } else {
        return { name: "login" }; // 否则,重定向到登录页面
      }
    },
  },
];

**注意:**不建议在一个路由配置中同时设置 componentredirect 属性,因为这两个属性在功能上是冲突的。

比如这样子写是不对的

const routes = [
  {
    path: "/",
    component: HomeView,
    redirect: "/user",
  },
];

为路由配置设置 redirect 属性时,它的目的是在用户访问该路由时自动导航到另一个路径,而不是同时加载该路由的组件。

如果确实想要在某些条件下显示 HomeView,并在其他条件下重定向到 “/user”,可能需要使用嵌套路由、动态路由匹配、编程式导航或其他逻辑来实现这一点。

const routes = [
  {
  path: "/",
  component: HomeView,
  beforeEnter: (to, from, next) => {
    // 假设这里有一些逻辑来决定是否应该重定向
    if (/* some condition */) {
      next({ path: '/user' }); // 重定向到 '/user'
    } else {
      next(); // 否则继续前往当前路由
    }
  }
}
]

上面的各个小节只提供基础路由知识,关于进阶版(包括导航守卫、路由元信息等),下次再说

4. pinia

Pinia 是一个符合直觉的,拥有组合式 API 的 Vue3 专属状态管理库。它允许你跨组件或页面共享状态。Pinia 可以看作是 Vuex 的升级版,拥有更加简洁的 API 和更好的 TypeScript 支持。

4.1 安装

npm install pinia
// src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const pinia = createPinia();
const app = createApp(App);

app.use(pinia);
app.mount("#app");

4.2 数据的起源和读取

state 是 Pinia 中存储数据的地方

// src/stores/count.ts
import { defineStore } from "pinia";

// UniqueId 这个参数要求是一个独一无二的名字,这里使用 UniqueId
export const useCountStore = defineStore("UniqueId", {
  // 为了完整类型推理,推荐使用箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断出它们的类型
      count: 0,
      name: "Eduardo",
      isAdmin: true,
      items: [1, 2, 3],
      hasChanged: true,
    };
  },
});

读取数据

<template>
  <h1>Home</h1>
  <h1>{{ store.count }}</h1>
  <h1>{{ store.name }}</h1>
</template>

<script setup lang="ts">
import { useCountStore } from "@/stores/count";
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCountStore();
console.log(store.$state); // 直接访问整个 state 对象
</script>

4.3 修改数据

1. 直接修改

优点:简单直接,可以快速地对 state 中的数据进行修改。

缺点:可能会绕过 Pinia 的响应系统,导致某些更新不会触发视图的重新渲染。此外,直接修改 state 可能会使代码难以追踪和维护。

接上面的例子:

// 视图更新为 999
store.count = 999;

2. $patch 修改

优点:提供了一种简洁、安全的方式来批量更新 state。使用$patch可以确保只有被明确指定的属性会被更新,从而减少了误修改的可能性。此外,$patch 方法内部进行了优化,可以高效地处理状态的更新。

缺点:对于单个属性的简单更新,使用 $patch 可能会显得过于繁琐。

store.$patch({
  count: store.count + 10,
});

不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。

store.$patch((state) => {
  state.items.push(4);
  state.hasChanged = true;
});
console.log(store.items); // [ 1, 2, 3, 4 ]

3. Actions 修改

优点:Actions 允许你在修改 state 之前执行额外的逻辑和错误处理。这使得状态更新更加可控和可预测。此外,Actions 还可以处理异步操作,这在许多实际应用中是非常必要的。

缺点:相比于直接修改或使用$patch 方法,通过 Actions 修改数据可能会更加复杂和繁琐。你需要定义 Actions 方法,并在其中编写更新 state 的代码。

import { defineStore } from "pinia";

export const useCountStore = defineStore("UniqueId", {
  state: () => {
    return {
      count: 0,
    };
  },
  actions: {
    increment() {
      this.count++;
    },
    // 当然,也可以在这里写异步函数
  },
});
<template>
  <h1>Home</h1>
  <h1>{{ store.count }}</h1>
  <button @click="store.increment">+</button>
</template>

<script setup lang="ts">
import { useCountStore } from "@/stores/count";
const store = useCountStore();
</script>

4.4 从 Store 解构

当你想从 Pinia store 中解构出多个状态(state)字段并在组件内部使用时,storeToRefs() 可以帮助你创建这些字段的响应式引用。

使用 storeToRefs() 可以避免直接使用解构赋值导致的响应性丢失问题。

<template>
  <h1>Home</h1>
  <h1>{{ count }}</h1>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCountStore } from "@/stores/count";
const store = useCountStore();
const { count } = storeToRefs(store);
</script>

与 toRefs() 的区别

  • toRefs() 是 Vue 3 提供的一个函数,用于将一个响应式对象转换为一个包含其所有属性作为单独响应式引用的对象。但 toRefs() 会对对象中的每个属性(包括方法)都进行 ref 包裹,这可能会导致不必要的性能开销。
  • storeToRefs() 是 Pinia 提供的,它只会对 store 中的状态(state)字段进行 ref 包裹,而忽略方法和其他非状态字段。这使得 storeToRefs() 在处理 Pinia store 时更加高效和有针对性。

4.5 Getters

  • Getter 完全等同于 store 的 state 的计算值,与 Vue 中的计算属性相似。推荐使用箭头函数,并且它将接收 state 作为第一个参数
  • 它基于 store 中的状态进行计算,返回一个新的结果。
  • Getters 是可响应式的,当 store 中的相关状态发生变化时,依赖于这些状态的 getters 会自动重新计算。
  • Getters 是可组合的,可以在 getters 中使用其他 getters 或 actions,以创建更复杂的计算逻辑。
import { defineStore } from "pinia";

export const useCountStore = defineStore("UniqueId", {
  state: () => {
    return {
      count: 2,
    };
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
});
<template>
  <h1>Home</h1>
  <h1>{{ store.doubleCount }}</h1>
</template>

<script setup lang="ts">
import { useCountStore } from "@/stores/count";
const store = useCountStore();
</script>

4.6 $subscribe

  • 监听变化:可以监听 store 中任何状态或 actions 的变化。
  • 回调函数:当数据发生变化时,会触发一个回调函数,你可以在这个函数中执行相应的操作。
  • 参数:回调函数通常接收两个参数,第一个参数是 mutation 对象(包含了变更的信息),第二个参数是变更后的 state。
import { defineStore } from "pinia";

export const useCountStore = defineStore("UniqueId", {
  state: () => {
    return {
      count: 0,
    };
  },
  actions: {
    increment() {
      this.count++;
    },
  },
});
<template>
  <h1>Home</h1>
  <h1>{{ count }}</h1>
  <button @click="store.increment">+</button>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCountStore } from "@/stores/count";

const store = useCountStore();
const { count } = storeToRefs(store);

// 使用 $subscribe 监听 count 的变化
store.$subscribe((mutation, state) => {
  console.log("Counter Store has changed:", mutation);
  console.log("New state:", state);
});
</script>

4.7 Option Store & Setup Store

这里有另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

这个是选项式写法(Option Store)

import { defineStore } from "pinia";

export const useCountStore = defineStore("UniqueId", {
  state: () => {
    return {
      count: 1,
    };
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++;
    },
  },
});

这个是组合式写法(Setup Store )

import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCountStore = defineStore("UniqueId", () => {
  // ref() 就是 state 属性
  const count = ref(1);

  // computed() 就是 getters
  const doubleCount = computed(() => count.value * 2);

  // function() 就是 actions
  function increment() {
    count.value++;
  }

  return { count, doubleCount, increment };
});

选择你觉得最舒服的那一个就好。Option Store 更容易使用,而 Setup Store 更灵活和强大。

5. 组件通信

5.1 props

Vue 组件可以接受来自父组件的数据,这些数据通过 props 进行传递。Props 是一种机制,它允许父组件向子组件传递数据。

<template>
  <h1>Father</h1>
  <Child :money="money" />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
import { ref } from "vue";

const money = ref("One million dollars");
</script>
<template>
  <h1>Child</h1>
  <div>money from Father:{{ money }}</div>
</template>

<script setup lang="ts">
defineProps(["money"]);
</script>

假设有些数据需要子组件传递给父组件,可以使用 props 传递一个函数给子组件,再由子组件操作函数带给父组件:

<template>
  <h1>Father</h1>
  <div>number from Child:{{ fromChild }}</div>
  <hr />
  <Child :getChild="getChild" />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
import { ref } from "vue";

const fromChild = ref<number | undefined>();

function getChild(v: number) {
  fromChild.value = v;
}
</script>
<template>
  <h1>Child</h1>
  <button @click="getChild(n)">To Father</button>
</template>

<script setup lang="ts">
defineProps(["getChild"]);

const n = 10;
</script>

5.3 自定义事件 $emit

子组件可以通过触发自定义事件(使用 $emit 方法)来向父组件传递值。这是组件间通信的一种常用方式,特别是当子组件需要通知父组件某些状态或数据变化时。

<template>
  <h1>Father</h1>
  <hr />
  <Child @child-event="handleChildEvent" />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";

function handleChildEvent(message: string) {
  console.log("子组件传递的消息:", message);
}
</script>
<template>
  <h1>Child</h1>
  <button @click="notifyParent">To Father</button>
</template>

<script setup lang="ts">
// 定义自定义事件
const emit = defineEmits(["child-event"]);

// 方法,用于触发自定义事件并传递值
function notifyParent() {
  emit("child-event", "Hello from child!");
}
</script>

5.4 mitt

mitt 是一个微小且高效的自定义事件发射/监听库,不是 Vue 官网的东西。它不依赖于任何特定的前端框架,可以在任何现代 JavaScript 环境中使用。更多详细信息请点击mitt

通过 npm 安装 mitt 库:

npm install mitt

先创建事件发射器:

// 引入 mitt
import mitt from "mitt";

// 创建事件发射器
const emitter = mitt();

export default emitter;

还是子组件的数据传到父组件的例子:

<template>
  <h1>Father</h1>
  <hr />
  <Child />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
import emitter from "@/utils/emitter";

// 监听子组件发送的事件
emitter.on("child-event", (data) => console.log(data)); // Hello from child!
</script>
<template>
  <h1>Child</h1>
  <button @click="notifyParent">To Father</button>
</template>

<script setup lang="ts">
import emitter from "@/utils/emitter";

function notifyParent() {
  // 在合适对的时候触发事件,向父组件传递数据
  emitter.emit("child-event", "Hello from child!");
}
</script>

当然,mitt 还有其他一些操作比如移除事件、监听所有事件等操作,这里不细说了。

5.5 组件 v-model

先看一个表单的双向数据绑定,就是将表单输入框的内容同步给 JavaScript 中相应的变量,通常都是这样:

<input v-model="text" />

其实是 v-model 指令帮我们简化了这一步骤,底层机制是这样:

<input :value="text" @input="event => text = event.target.value" />

先绑定值,再添加一个输入事件监听。

知道了这点后,接下来可以探讨 组件 v-model 了,可以在组件上使用以实现双向绑定的v-model

<template>
  <h1>Father</h1>
  <div>Father message:{{ message }}</div>
  <hr />
  <!-- 正常使用这么写就可以了 -->
  <Child v-model="message" />
  <!-- 底层原理 -->
  <!-- <Child :model-value="message" @update:model-value="message = $event" /> -->
</template>

<script setup lang="ts">
import { ref } from "vue";
import Child from "@/components/Child.vue";

const message = ref("lena");
</script>
<template>
  <h1>Child</h1>
  <input
    type="text"
    :value="modelValue"
    @input="
      emit('update:modelValue', ($event.target as HTMLInputElement).value)
    "
  />
</template>

<script setup lang="ts">
defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

这里发生的事情如下:

  • :modelValue="message":这是将父组件中的 message 数据属性绑定到子组件的 modelValue prop 上。这意味着子组件将接收一个名为 modelValue 的 prop,其值为父组件中的 message

  • @update:model-value="message = $event":这是一个事件监听器,用于监听子组件触发的 update:modelValue 事件。当这个事件被触发时,它接收一个参数(通常是一个新的值),这个参数在事件处理函数中被引用为 $event。然后,这个新的值被赋值给父组件的 message 数据属性。

子组件这么写确实有点麻烦,然而,这样写有助于理解其底层机制。从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:

<template>
  <h1>Child</h1>
  <input type="text" v-model="message" />
</template>

<script setup lang="ts">
const message = defineModel();
</script>

这样写可以实现一样的效果。假如需要多个 v-model 绑定:

<Child v-model:first-name="first" v-model:last-name="last" />
<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

<script setup lang="ts">
const firstName = defineModel("firstName");
const lastName = defineModel("lastName");
</script>

5.6 透传 Attributes

“透传 attribute”是指将父组件的未被子组件声明的为 propsemits 的 属性 或者 v-on 事件监听器传递到子组件的根元素上。最常见的例子就是 classstyleid。这在创建可重用组件时特别有用,因为它允许父组件动态地设置子组件根元素的属性。

<template>
  <h1>Father</h1>
  <hr />
  <Child class="blue" />
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
</script>
<!-- 子组件假设只有一个根元素 button -->
<template>
  <button>click</button>
</template>

<script setup lang="ts"></script>

<style>
.red {
  background-color: red;
}
.blue {
  background-color: blue;
}
</style>

父组件可以设置 class="blue"或者 class="red" 来决定子组件的背景颜色。像上面的例子,子组件渲染完成后是 <button class="blue">click</button>

当然,子组件也可以在 JavaScript 中访问透传 Attributes

<script setup lang="ts">
import { useAttrs } from "vue";

const attrs = useAttrs();
console.log(attrs); // {class: 'blue'}
</script>

6.6 $refs$parent

$refs

$refs 是一个对象,用于直接访问已注册引用的子组件。引用信息注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用将直接是该 DOM 元素;如果用在子组件上,引用就指向该子组件实例。这里介绍子组件的用法。

<template>
  <h1>Father</h1>
  <hr />
  <Child ref="child" />
  <Son ref="son" />
  <button @click="getChild($refs)">get child</button>
</template>

<script setup lang="ts">
import { ref } from "vue";
import Child from "@/components/Child.vue";
import Son from "@/components/Son.vue";

const child = ref();
const son = ref();

function getChild(v: { [key: string]: any }) {
  for (let key in v) {
    v[key].doSomething();
  }
}
</script>
<template>
  <h1>Child</h1>
</template>

<script setup lang="ts">
function doSomething() {
  console.log("Doing something in child component!");
}

// 暴露出去的属性或者方法
defineExpose({
  doSomething,
});
</script>
<template>
  <h1>Son</h1>
</template>

<script setup lang="ts">
function doSomething() {
  console.log("Doing something in son component!");
}

defineExpose({
  doSomething,
});
</script>

虽然不应该在 Vue 组件的模板或计算属性中直接访问 $refs 来获取子组件或 DOM 元素的引用。这是因为 $refs 是响应式系统之外的,它们不会触发视图的更新,并且可能在组件的生命周期中的不同时间点变得不可用。但还是看看吧。

父组件在组件模板通过$refs引用 2 个子组件,目的是要调用子组件的方法,注意要等组件挂载完成后才能使用。子组件的属性或方法默认是关闭的,不过可以通过defineExpose指定要暴露出去的属性或者方法,这样父组件就可以访问了。

可能你注意到了getChild方法使用了 for 循环,这是因为 $refs 包含了所有使用 ref 标记的组件或 DOM 元素的引用。

$parent

$refs类似,,这次是子组件访问父组件的属性或者方法。还是不太建议使用。

<template>
  <h1>Father</h1>
  <hr />
  <Child />
</template>

<script setup lang="ts">
import { ref } from "vue";
import Child from "@/components/Child.vue";

const tellYour = ref("I Am Your Father");
defineExpose({
  tellYour,
});
</script>
<template>
  <h1>Child</h1>
  <button @click="hear($parent)">BTN</button>
</template>

<script setup lang="ts">
function hear(v: any) {
  console.log(v.tellYour);
}
</script>

6.7 provideinject

provideinject 是 Vue.js 提供的一对选项,用于在组件树中实现跨层级的数据传递,而不必显式地通过每一层组件逐层传递 props 或触发事件。这对选项特别适用于插槽、高阶组件或库的开发,以及在深层嵌套的组件之间共享某些状态或配置。

provide 选项是一个对象或返回一个对象的函数。该对象包含可注入其子级的属性。你可以将这些属性视为从祖先组件“提供”给所有子孙后代的依赖项。

inject 选项是一个字符串数组或一个对象,用于从祖先组件中“注入”依赖项。每个字符串代表一个要从祖先组件中注入的属性名。

<template>
  <h1>Father</h1>
  <hr />
  <Child />
</template>

<script setup lang="ts">
import { ref, provide } from "vue";
import Child from "@/components/Child.vue";

const name = ref("lena");

provide("name", name);
</script>
<template>
  <h1>Child</h1>
  <div>{{ name }}</div>
</template>

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

const name = inject<string>("name");
</script>

6.8 插槽 Slots

“Slots”是“slot”的复数形式,它表示组件中可以有多个插槽。在 Vue 中,一个组件可以定义多个插槽,用于在父组件中插入不同的内容。使用“Slots”作为复数形式,更准确地表达了插槽的这一特性。

默认插槽(Default Slots)

默认插槽是没有名字的插槽,其实有个隐藏的名字 default,它会在父组件没有指定名称时自动接收内容。

<template>
  <h1>Father</h1>
  <hr />
  <Child> I am Your Father </Child>
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
</script>
<template>
  <h1>Child</h1>
  <slot>
    默认内容
    <!-- 默认内容,没有提供任何插槽内容时展示 -->
  </slot>
</template>

具名插槽(Named Slots)

具名插槽允许你在子组件中定义多个插槽,并在父组件中通过 v-slot 指令指定要插入到哪个插槽。

<template>
  <h1>Father</h1>
  <hr />
  <Child>
    <!-- 这里的 header 和 footer 位置调换是故意的 -->
    <template v-slot:footer>
      <p>这是 footer 插槽的内容</p>
    </template>
    <template #header>
      <p>这是 header 插槽的内容</p>
    </template>
  </Child>
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
</script>
<template>
  <h1>Child</h1>
  <header>
    <!-- 具名插槽 header -->
    <slot name="header"></slot>
  </header>

  <footer>
    <!-- 具名插槽 footer -->
    <slot name="footer"></slot>
  </footer>
</template>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

作用域插槽(Scoped Slots)

作用域插槽允许你在插槽中访问子组件的数据。在子组件的 <slot> 标签上,你可以使用 v-bind 指令(或简写为 :)绑定数据到插槽的 v-slot 的参数。

<template>
  <h1>Father</h1>
  <hr />
  <Child v-slot="slotProps">
    <slot>
      {{ slotProps.count }}
    </slot>
  </Child>
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
</script>
<template>
  <h1>Child</h1>
  <slot :count="count"></slot>
</template>

<script setup lang="ts">
const count = 1;
</script>

这个是具名作用域插槽

<template>
  <h1>Father</h1>
  <hr />
  <Child>
    <!-- 访问作用域插槽的数据 -->
    <template v-slot:header="headerProps">
      {{ headerProps.header }}
    </template>

    <!-- 这里使用简写和解构 -->
    <template #footer="{ footer }">
      {{ footer }}
    </template>
  </Child>
</template>

<script setup lang="ts">
import Child from "@/components/Child.vue";
</script>
<template>
  <h1>Child</h1>
  <!-- 绑定数据到具名插槽 -->
  <slot name="header" :header="header"></slot>
  <br />
  <slot name="footer" :footer="footer"></slot>
</template>

<script setup lang="ts">
const header = "header";
const footer = "footer";
</script>

动态插槽名(Dynamic Slot Names)

使用变量或表达式来动态地绑定插槽名,这在创建灵活的组件时非常有用。

<template>
  <Child>
    <!-- dynamicSlotName 表示动态插槽的名字 -->
    <template v-slot:[dynamicSlotName]> ... </template>
    <template #[dynamicSlotName]> ... </template>
  </Child>
</template>

6. 一些 响应式 API

6.1 shallowRef 与 shallowReactive

shallowRef

  • 只有对 .value 的访问是响应式的
  • 如果传入的是基本数据类型(如数字、字符串等),shallowRef 的行为与 ref 相似。
  • 如果传入的是对象或数组,shallowRef 不会对其内部属性进行响应式处理。
<template>
  <h1>state.count:{{ state.count }}</h1>
  <button @click="noChange">不会触发更改</button>
  <button @click="change">会触发更改</button>
</template>

<script setup lang="ts">
import { shallowRef, watch } from "vue";

const state = shallowRef({ count: 1 });
function noChange() {
  // 不会触发更改
  state.value.count++;
}
function change() {
  // 会触发更改
  state.value = { count: 3 };
}

watch(state, () => console.log("state change"));
</script>

shallowReactive

  • shallowReactive 用于创建一个对对象的第一层属性进行响应式处理的响应式对象。
  • 它不会递归地将对象内部的属性转换为响应式。
<template>
  <h1>foo:{{ state.foo }}</h1>
  <h1>bar:{{ state.nested.bar }}</h1>
  <button @click="noChange">不会触发更改</button>
  <button @click="change">会触发更改</button>
</template>

<script setup lang="ts">
import { shallowReactive, watch, isReactive } from "vue";

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
});

// 下层嵌套对象不会被转为响应式
console.log(isReactive(state.nested)); // false

function noChange() {
  // 不是响应式的,不会触发更改
  state.nested.bar++;
}
function change() {
  // 更改状态自身的属性是响应式的,会触发更改
  state.foo++;
}

watch(state, () => console.log("state change"));
</script>

注意:点击 noChange 后再点击 change,视图 bar 还是会改变。尽管 state.nested.bar 的修改没有被直接观察到,但是在重新渲染组件时,Vue 会重新读取整个 state 对象的属性值来更新视图。这意味着即使你之前通过非响应式方式修改了 state.nested.bar 的值,当 Vue 渲染函数再次运行时,它会看到这个外部改变,并将其反映到视图中。所以,当你看到 bar 的值在点击 “会触发更改” 后也更新了,实际上是 Vue 重新渲染时同步了最新值到 DOM。

6.2 readonly 与 shallowReadonly

readonly

readonly 函数用于创建一个只读的响应式对象(或者普通对象),这意味着该对象及其所有嵌套属性都不能被修改。

<template>
  <div>
    <p>Readonly Foo: {{ readonlyState.foo }}</p>
    <p>Readonly Nested Bar: {{ readonlyState.nested.bar }}</p>
    <button @click="tryModifyReadonly">尝试修改 Readonly</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, readonly } from "vue";

const state = reactive({
  foo: "Hello",
  nested: {
    bar: "World",
  },
});

const readonlyState = readonly(state);

function tryModifyReadonly() {
  // 尝试修改 readonlyState 的属性将不会生效
  readonlyState.foo = "Modified"; // 不会有任何效果,并可能触发警告
  readonlyState.nested.bar = "Modified Nested"; // 同样不会有任何效果,并可能触发警告

  // 打印原始状态以确认没有改变
  console.log(state.foo); // 输出 "Hello"
  console.log(state.nested.bar); // 输出 "World"
}
</script>

shallowReadonly

shallowReadonly 函数也用于创建只读对象,但与 readonly 不同,它只将对象的第一层属性设置为只读。

<template>
  <div>
    <p>Shallow Readonly Foo: {{ shallowReadonlyState.foo }}</p>
    <p>Shallow Readonly Nested Bar: {{ shallowReadonlyState.nested.bar }}</p>
    <button @click="tryModifyShallowReadonly">尝试修改 Shallow Readonly</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, shallowReadonly } from "vue";

const state = reactive({
  foo: "Hello",
  nested: {
    bar: "World",
  },
});

const shallowReadonlyState = shallowReadonly(state);

function tryModifyShallowReadonly() {
  // 尝试修改 shallowReadonlyState 的顶层属性将不会生效
  shallowReadonlyState.foo = "Modified"; // 不会有任何效果,并可能触发警告

  // 但我们可以修改嵌套属性,因为 shallowReadonly 只对顶层属性提供只读保护
  // 这将改变嵌套对象的状态,但视图可能不会更新(除非 nested 也是响应式的,所以这里更新了视图)
  // 由于 nested 的响应性,任何对 nested 内部属性的修改都会触发视图更新。
  shallowReadonlyState.nested.bar = "Modified Nested";

  // 打印原始状态以确认改变
  console.log(state.foo); // 输出 "Hello"
  console.log(state.nested.bar); // 输出 "Modified Nested"
}
</script>

6.3 toRaw 与 markRaw

toRaw

toRaw 函数用于获取被 Vue 3 响应式系统代理的原始(非响应式)对象。toRaw 返回的是被代理的原始对象的一个引用,而不是一个复制或新的对象。

这意味着,如果你修改了返回的原始对象,那么原始的响应式对象也会相应地更新。这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。

<template>
  <div>
    <p>原始对象: {{ rawObject.text }}</p>
    <p>响应式对象: {{ reactiveObject.text }}</p>
    <button @click="modifyRawObject">修改原始对象</button>
  </div>
</template>

<script setup lang="ts">
import { reactive, toRaw } from "vue";

// 创建一个响应式对象
const reactiveObject = reactive({
  text: "Hello, Vue!",
});

// 获取响应式对象的原始对象
const rawObject = toRaw(reactiveObject);

function modifyRawObject() {
  // 修改原始对象
  rawObject.text = "原始对象被修改了!";
  // 因为我们修改了原始对象,所以响应式对象也会自动更新,但视图不会更新
  console.log(reactiveObject.text); // 输出: "原始对象被修改了!"
}
</script>

markRaw

markRaw函数用于告诉 Vue 的响应性系统不要对某个对象进行转换或追踪其响应性。当你有一个对象,并且你确定你不需要它成为响应式对象时,你可以使用markRaw来标记它。

<template>
  <div>
    <p>非响应式对象: {{ nonReactiveObject.text }}</p>
    <button @click="modifyNonReactiveObject">修改非响应式对象</button>
  </div>
</template>

<script setup lang="ts">
import { markRaw } from "vue";

// 创建一个非响应式对象
const nonReactiveObject = markRaw({
  text: "Hello, Vue!",
});

function modifyNonReactiveObject() {
  // 修改非响应式对象
  nonReactiveObject.text = "非响应式对象被修改了!";
  // 注意:视图不会更新,因为nonReactiveObject不是响应式的
  console.log(nonReactiveObject.text); // 输出: "非响应式对象被修改了!"
}
</script>

6.4 customRef

customRef 允许你创建自定义的 ref,并显式地控制依赖追踪和触发响应的方式。这在某些需要自定义数据响应行为的场景中非常有用。

customRef 接受一个工厂函数作为参数,该工厂函数接受两个参数:tracktriggertrack 用于收集依赖,trigger 用于触发响应。工厂函数需要返回一个具有 getset 方法的对象。

例如:

import { customRef } from "vue";

function myCustomRef(value, ...args) {
  return customRef((track, trigger) => {
    return {
      get() {
        track(); // 通知 Vue 追踪这个 ref 的变化
        return value;
      },
      set(newValue) {
        // 在这里可以添加自定义逻辑
        value = newValue; // 更新值
        trigger(); // 通知 Vue 重新渲染依赖于这个 ref 的部分
      },
    };
  }, ...args); // 可以传递额外的参数给工厂函数
}

创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

<template>
  <div>
    <input v-model="debouncedValue" placeholder="Type something and wait..." />
    <p>Debounced value: {{ debouncedValue }}</p>
  </div>
</template>
<script setup lang="ts">
import { customRef, ref } from "vue";

function useDebouncedRef<T>(value: T, delay = 200) {
  let timeout: number;

  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      },
    };
  });
}

// 原始输入值
const inputValue = ref("");

// 使用防抖功能的 ref
const debouncedValue = useDebouncedRef(inputValue.value);
</script>

7. 新组件

Teleport

Teleport 组件允许你将子组件渲染到 DOM 树中的任何位置,而不仅仅是其父组件的模板中。这类场景最常见的例子就是全屏的模态框。

<Teleport> 接收一个 to 属性来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。

<template>
  <h1>Father</h1>
  <MyModal />
</template>

<script setup lang="ts">
import MyModal from "@/components/MyModal.vue";
</script>
<template>
  <button @click="open = true">Open Modal</button>

  <!-- 把以下模板片段传送到 body 标签下 -->
  <Teleport to="body">
    <div v-if="open" class="modal">
      <p>Hello from the modal!</p>
      <button @click="open = false">Close</button>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { ref } from "vue";

const open = ref(false);
</script>

<style scoped>
.modal {
  position: fixed;
  z-index: 999;
  top: 50%;
  left: 50%;
  width: 300px;
  margin-left: -150px;
  border: 2px solid red;
}
</style>

其他

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from

  • keyCode 作为 v-on 修饰符的支持

  • v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync

  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化

  • 移除了$on$off$once 实例方法

  • 移除了过滤器 filter

  • 移除了$children 实例 propert

附录(一些东西)

1、Vite Plugin 调试工具

还在为谷歌浏览器不能访问谷歌商店而苦恼,还在为不能安装 Vue.js devtools 调试工具而感到沮丧? Vite Plugin 帮你解决。

安装

npm add -D vite-plugin-vue-devtools

使用

// vite.config.ts
// 配置 Vite
import { defineConfig } from "vite";
import vueDevTools from "vite-plugin-vue-devtools";

export default defineConfig({
  plugins: [vueDevTools()],
});

启动项目后就自带 Vue 调试工具了,非常好用。更多详情点击 Vue DevTools

2、 一键启动

这个是和 Windows 相关的。

在学习的过程中,有时候需要通过右键 VS Code 打开几个文件夹,或许还需要打开几个浏览器(我就是这样),更甚者还带有其他软件(音乐、视频等)。每次启动电脑后一顿点点点,学习的热情在这个过程中慢慢消散。。。

我们可以使用批处理脚本 .bat 解决这个问题,脚本写好后,一次点击,梦想启动。

@echo off
start "" "C:\Program Files\Software1\Software1.exe"
start "" "C:\Program Files\Software2\Software2.exe"
start "" "C:\Program Files\Microsoft VS Code\Code.exe" "C:\path\to\your\folder1"
start "" "C:\Program Files\Microsoft VS Code\Code.exe" "C:\path\to\your\folder2"

在上面的示例中,Software1.exeSoftware2.exe 是你要打开的软件的路径,你需要将它们替换为实际的软件路径。

最后两行是 VS Code 的路径和你想要使用VS Code打开的文件夹的路径。

路径怎么找?右键文件夹或者软件,点击属性(R),打开文件所在的位置(F),复制路径后 CV 就可以了。提醒一下,启动程序一定要带上,比如上面的 .exe 文件。全程无毒副作用,使用简单,方便快捷,值得尝试。

不想学了,想一键关闭这些文件或者软件,实现比较麻烦,建议拔电源线或者关机。

备忘录:

1 v-if v-for 的优先级问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值