前言:你的阅读速度足够快,10分钟内就可以读完这篇文章。总体来说,内容比较粗浅(入门级),所以如果想了解更多,请访问官网。
1. Vue3 和 Vue2 的区别
双向数据绑定原理:
Vue2使用Object.defineProperty()进行数据劫持,并结合发布和订阅方法实现双向数据绑定。然而,这种方法有一些缺点。例如,要启用添加或删除属性,您必须使用特殊的API,例如Vue.set/delete。 es6 中没有其他数据结构支持等待。 Vue3使用代理通过ref或reactive将数据转换为响应式数据。代理监视整个对象,可以拦截对对象的所有操作,例如添加属性、删除属性以及更改数组中的内部数据。另外,代理还可以存储ins、闭包等内容,并且可以直接绑定整个对象以提高效率。 组件模板:
Vue2要求组件模板有唯一的顶级元素封装,如templatediv./div/template。 Vue3 支持片段。这意味着一个组件可以有多个根节点,而无需额外的包装元素,例如templatediv./divp./p/template。 API类型:
Vue2 使用Options API 来分隔代码中的不同属性,例如数据、计算、方法等。 Vue3 引入了Composition API。数据和方法在setup函数中定义,统一通过return返回。与旧的API使用属性进行分组相比,统一的API使用方法进行分区,使代码更加简单、清晰。 生命周期:
Vue2生命周期钩子包括beforeCreate、created、beforeMount、mounted等。 Vue3 的生命周期钩子已使用组合API 进行了修改。例如setup函数已经被beforeCreate替代并创建了。其他生命周期钩子也已重命名,但它们的功能本质上是相同的。 性能优化:
Vue3 有很多性能优化,包括对Diff 算法的改进,使得虚拟DOM 比较和更新更加高效。 其他新功能:
Vue3 引入了Teleport 组件,它允许组件的内容渲染到DOM 树中的任何位置,而不仅仅是其子元素。 Vue3 还支持Suspense 组件,用于在加载异步组件时显示替代内容。还有其他新的API。查看API 参考。
综上所述,Vue3在Vue2的基础上有很多改进和优化,包括数据绑定原理、组件模板、API类型、生命周期、性能优化以及新功能的引入。这些改进显着提高了Vue3的开发效率、代码简洁性和性能。
将Vue2 和Vue3 视为不同但有些相似的框架。有关两者之间差异的更多信息,请参阅Vue 3 迁移指南。学习Vue 3 的推荐方法是阅读新文档。
2. 基本语法
2.1 setup
什么是设置?
setup 是一个特殊的组件选项,在创建组件之前调用。 setup 函数提供对组件属性和上下文(属性、槽、发出等)的访问。组合API 函数(ref、reactive、compute 等)可用于定义状态、计算属性、方法等。返回的所有内容都可以直接在模板中使用(例如,通过return 语句返回的状态和计算属性)。在设置中访问它是未定义的。
!– 2.1.1 —
模板
按钮@click=\’count++\'{{ count }}/按钮
/模板
脚本语言=\’ts\’
从\’vue\’ 导入{ ref }。
默认导出{
name: \’家\’,
环境() {
常数计数=ref(0);
//返回值暴露给模板和其他可选的API 挂钩
返回{
数数,
};
},
安装(){
console.log(this.count); //0
},
};
/剧本
2.2 setup 语法糖 script setup
2.1.1 中的示例可以使用语法糖重写。
!– 2.2.1 —
模板
按钮@click=\’count++\'{{ count }}/按钮
/模板
脚本设置lang=\’ts\’ name=\’Home\’
从\’vue\’ 导入{ ref, onMounted };
常数计数=ref(0);
已安装(()={
console.log(count.value); //0
});
/剧本
其中,当前组件名称name=\’Home\’可以使用插件如2.2.1那样编写。否则,需要编写脚本lang=\’ts\’export default {name:\’Home\’}/script。分别扔了。使用插件如下:
安装插件npm i vite-plugin-vue-setup-extend -D。添加文件vite.config.ts
//2.2.2
从\’vite\’ 导入{defineConfig};
从“vite-plugin-vue-setup-extend”导入VueSetupExtend。
导出默认的defineConfig({
plugins: [VueSetupExtend()],
});
2.3 基本类型的响应式数据 ref()
通常,定义响应式基本类型,例如数字、字符串和布尔值。在您的设置中使用.value 来获取或更改该值。该值可以直接在模板中使用。 ref() 还可以定义对象类型的响应数据,但不推荐这样做。
模板
{{ 福}}
/模板
脚本设置lang=\’ts\’ name=\’Home\’
从\’vue\’ 导入{ ref }。
让foo=ref(0);
console.log(foo.value); //0
foo.value=9;
控制台.log(foo.value); //9
/剧本
2.4 对象类型的响应式数据 reactive()
定义反应式对象类型。反应性是“深”的。影响所有嵌套属性。
脚本设置lang=\’ts\’ name=\’Home\’
从“vue”导入{reactive}。
const obj=反应式({
foo: 0,
栏: {
计数: 1,
},
});
console.log(obj.foo, obj.bar.count); //0 1
obj.bar.count=999;
console.log(obj.bar.count); //999
/剧本
下面的代码直观上没问题,但有点违反直觉。
模板
{{ obj.foo }}
按钮@click=\’change\’btn/button
/模板
脚本设置lang=\’ts\’ name=\’Home\’
从“vue”导入{reactive}。
让obj=反应式({
foo: 0,
});
函数变化(){
obj=反应式({
foo: 100,
});
}
/剧本
我试图通过单击按钮时调用更改函数来更改foo 对象的obj 属性的值。然而,这种方法存在重大问题。我们不是修改现有的反应对象,而是将obj 重新分配给新的反应对象。因此,页面不会刷新,显示仍为0。
解决方案是直接更改对象的属性来触发视图更新,而不是将新的反应对象重新分配给变量。
函数变化(){
obj.foo=100;
//也可以这样写,直接改变源对象
//Object.assign(obj, { foo: 100 });
}
2.5 toRef() 和 toRefs()
toRef() 可以根据响应对象的属性创建相应的引用。
模板
{{ p.name }}
/模板
脚本设置lang=\’ts\’ name=\’Home\’
从“vue”导入{reactive,toRef};
const p=反应式({
name:“萨莎”,
年龄: 18,
});
让name=p.name。
名称=\’莉娜\’;
/剧本
如果上面的代码中name=\’lena\’ 发生变化,视图显然不会更新。这不是响应性值变化。
为此,请更改最后两行代码,如下所示:
让refName=toRef(p, \’名称\’);
refName.value=\’莉娜\’;
这样您就可以分离响应对象的属性并将它们转换为响应值。
toRefs() 主要用于将reactive创建的响应对象的属性转换为另一个响应引用(ref)。
模板
{{ p.name }}
{{ 页}}
/模板
脚本设置lang=\’ts\’ name=\’Home\’
从“vue”导入{reactive,toRefs};
const p=反应式({
name:“萨莎”,
年龄: 18,
});
const refsP=toRefs(p);
//视图更新为更改后的值
refsP.name.value=\’莉娜\’;
refsP.age.value=19;
/剧本
2.6 计算属性 computed()
计算属性默认是只读的,但当然可以修改。接受getter 函数。这是一个有返回值的函数,或者是一个带有get 和set 函数的对象。
!– 如何编写函数–
脚本设置lang=\’ts\’ name=\’Home\’
从“vue”导入{ref,计算};
const foo=ref(1);
常量栏=ref(2);
const plus=计算(()=foo.value + bar.value);
console.log(plus.value); //3
/剧本
//对象写入方法
常量加=计算({
get:()=foo.value + bar.value,
设置(v){
//在此写入任何更改
},
});
2.7 侦听器 watch()
监听一个或多个反应式数据源,并在数据源发生变化时调用指定的回调函数。
watch(source,callback,option) 函数接受三个参数,下面简要介绍。
1.source ——监听源
返回值、ref、反应式对象(可能是反应式).或由上述类型的值组成的数组的函数
2.callback —— 参数变化时调用的回调函数
(新值,旧值,onCleanup)={
//newValue 新值
//oldValue 旧值
//onCleanup 副作用清理回调函数
};
3. 选项—— 是支持以下选项的对象:
立即:监听创建后立即触发回调。第一次调用时,旧值未定义。 deep:如果源是对象,则强制进行深度遍历,以便当深度级别发生变化时触发回调。 flash:调整回调函数的刷新时机。 onTrack/onTrigger:调试侦听器依赖项。 Once:回调函数只执行一次。第一次执行回调函数后,监听器会自动停止。
注意事项:
当你监听ref定义的对象时,你监听的是该对象的地址。如果你想听那个属性,你需要添加深度。
脚本设置lang=\’ts\’
从\’vue\’ 导入{ ref, watch };
常量状态=ref({
foo: 1,
栏: {
name:“萨莎”,
},
});
钟(
状态,
()={
//做一点事
},
{
深:真实,
}
);
/剧本
您无法直接监听: 等反应式对象上的属性值。
脚本设置lang=\’ts\’
从\’vue\’ 导入{reactive, watch};
常量状态=反应性({
foo: 1,
});
//写错了
watch(state.foo, ()=console.log(\’状态改变\’));
/剧本
您需要使用返回属性的getter 函数
钟(
()=状态.foo,
()=console.log(\’状态改变\’)
);
默认情况下,监听器回调会在父组件(如果有)更新之后、所属组件的DOM 更新之前调用。这意味着当您尝试在侦听器回调中访问所属组件的DOM 时,该DOM 将处于未更新状态。
如果您想在侦听器回调中由Vue 更新组件的DOM 后访问该组件的DOM,则必须指定lush: \’post\’
2.8 侦听器 watchEffect()
与watch 一样,watchEffect 会自动跟踪回调中的反应性依赖项,而无需指定侦听器的源。
watchEffect(callback,option) 仅接受两个参数
回调是onCleanup 执行的副作用函数。没有新值或旧值。
该选项可用于调整副作用闪烁的刷新时序以及调试副作用的onTrack/onTrigger 依赖性。
常量状态=反应性({
foo: 1,
酒吧: 2,
});
//自动跟踪foo,bar
观察效果(()={
if (state.foo 1 || state.bar 2) {
//做一点事
}
});
2.9 watch 和 watchEffect 的区别
在Vue 3 中,watch 和watchEffect 都是用于事后观察和响应数据变化的API,但它们之间有一些重要的区别。它们之间的主要区别是:
自动跟踪依赖关系。
watchEffect:在执行过程中自动跟踪依赖的响应数据,并在这些数据发生变化时重新运行。您不必显式指定要观察的数据;Vue 会自动处理它。 watch:必须显式指定要监视的数据源(反应式引用、计算属性、函数返回值等),并在这些数据源发生变化时执行回调函数。 执行时间处理时间:
watchEffect:组件加载后(执行设置函数)运行一次,依赖数据更改时运行一次。 watch: 回调函数不会在组件加载时立即执行,而是仅在被监视的数据源发生变化时执行。 停止观察:
两者都返回一个观察停止函数,可以调用该函数来停止观察。在Vue 组件中,当组件被卸载时,Vue 会自动停止组件内创建的所有watch 和watchEffect 观察。 性能考虑:
watchEffect 会自动跟踪依赖关系,这在某些情况下可能会导致不必要的重新运行,尤其是在大型复杂的应用程序中。手表可以让您更好地控制监视哪些数据源,并允许您在这些数据源发生变化时执行特定逻辑,从而提高性能。 使用场景:
watchEffect:适用于需要监控多个数据源并在其中一个数据源发生变化时执行一些逻辑的情况。它会自动跟踪依赖关系,因此您不必显式指定要观察的数据源。 watch:适用于需要显式监控一个或多个数据源,并在数据源发生变化时执行特定逻辑的情况。您可以更好地控制要监视的数据源以及何时运行回调函数。 明显的副作用:
watchEffect 的回调函数可以使用onInvalidate 函数来注册一个清理函数,该函数将在watchEffect 停止监控之前被调用。这可用于产生副作用,例如清除计时器、取消网络请求等。 watch 没有直接的onInvalidate 函数,但您可以在回调函数中手动管理副作用清理。
总的来说,watch 和watchEffect 提供了多种方式来观察和响应Vue 组件中的数据变化。您应该根据您的具体需求选择要使用的API。
2.10 何时使用 watch 或 watchEffect
使用手表时:
确切地知道要监视的数据:如果您确切地知道要监视对象的哪些反应性引用或属性,那么使用watch 是合适的。您可以将要观察的数据作为第一个参数传递给观察,并定义一个回调函数来处理数据的更改。
需要访问旧值:监控回调函数可以访问数据更改前后的旧值和新值。这在需要比较新旧值以执行某些操作的场景中非常有用。
性能优化:watch 通常比watchEffect 更高效,因为它仅在指定数据源发生变化时才执行其回调函数。如果您只需要观察少数数据源,并且不需要每次渲染组件时都重新运行副作用函数,那么使用watch 可以减少不必要的计算。
使用watchEffect时:
自动跟踪依赖关系:如果您希望副作用函数自动跟踪它们内部依赖的反应性数据,并在数据更改时重新运行副作用函数,那么使用watchEffect 是合适的。 watchEffect 自动收集副作用函数中使用的反应数据作为依赖项,并在这些依赖项发生变化时重新执行副作用函数。
无需显式指定数据源。如果你不知道需要监控哪个数据源,或者你的副作用函数依赖于多个数据源并且这些数据源将来可能会发生变化,那么watchEffect 可以帮助你在这种情况下更灵活地处理。
组件加载后立即运行:watchEffect 在组件加载后立即运行一次副作用函数(即执行setup 函数),并在依赖数据发生更改时再次运行。这对于需要在组件加载后立即运行一些初始化逻辑的场景非常有用。
总结:
当您确切知道要监视哪些数据源并且需要访问旧值或执行性能优化时,请使用watch。如果您想要自动跟踪副作用函数的依赖关系并在依赖关系发生变化时重新运行该副作用函数,或者如果您不确定需要观察哪些数据源并且希望在它们发生变化时立即进行观察。如果要执行逻辑,请使用watchEffect。该组件将被加载。
2.11 模板引用 ref
用于常规DOM 元素以引用元素本身
模板
p ref=\’p\’你好/p
/模板
脚本设置lang=\’ts\’
从\’vue\’ 导入{ ref, onMounted };
const p=ref();
//ref 本身是作为渲染函数的结果而创建的,因此必须等到组件挂载后才能访问它。
onMounted(()=console.log(p.value)); //phello/p
/剧本
当在子组件中使用时,引用将是子组件的实例。
模板
子参考=\’c\’/
/模板
脚本设置lang=\’ts\’
从“@/components/Child.vue”导入子项。
从\’vue\’ 导入{ ref, onMounted };
const c=ref();
onMounted(()=console.log(c.value));
/剧本
2.12 props 写法
Props 是子组件从父组件接收的数据。子组件无法更改props 值。这里主要介绍三种props的写法。
!– 组件–
脚本设置lang=\’ts\’
//1. 轻松接收
const props=defineProps([\’foo\’]);
//或限制类型
//const 道具=defineProps({
//foo: 字符串,
//});
console.log(props.foo);
//2. 使用泛型参数定义props 的类型
常量道具=defineProps{
foo: 字符串;
酒吧?号。
}();
//当使用基于类型的声明(泛型参数)时,
//不再可能为props 声明默认值。
//3. 可以用withDefaults编译器宏解决
导出接口Props {
消息? 字符串;
标签? 字符串[];
}
const props=withDefaults(definePropsProps(), {
msg: \’你好\’,
标签:()=
[\”one\”, \”two\”],
});
</script>
defineProps 是只能在 <script setup> 中使用的编译器宏,不需要导入。还有其他一些宏,这里不展示。
2.13 生命周期
下面是一张 选项式API(Options API) 的生命周期图
vue3 推荐使用 组合式API(Composition API)生命周期函数,setup替代了 beforeCreate 和created。
onMounted: 相当于 Options API 中的 mounted 钩子。
onUpdated: 相当于 Options API 中的 updated 钩子。
onUnmounted: 相当于 Options API 中的 unmounted 钩子。
onBeforeMount: 相当于 Options API 中的 beforeMount 钩子。
onBeforeUpdate: 相当于 Options API 中的 beforeUpdate 钩子。
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 的主要组成部分包括:
ref 和 reactive:用于创建响应式引用和对象。computed:用于创建计算属性。watch 和 watchEffect:用于侦听响应式数据的变化并执行副作用。provide 和 inject:用于依赖注入。
现在我要实现一个简单的计算加减功能
<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>
这个功能使用到了 ref 和 computed ,我们可以自定义 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 Hooks,Vue3为什么要这么设计?请看官方文档 和 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 个路由组件(通常存放在pages 或 views文件夹)HomeView 和 AboutView
createWebHistory 表示基于 HTML5 history API 创建的路由实例,无特殊字符,需要后端配置的 URL。入参默认为空,import.meta.env.BASE_URL这个变量是 Vite 和 Vue 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 RouterLink 和 RouterView
RouterLink 用于实现路由跳转,RouterView 用于动态显示根据当前路由匹配到的组件内容。两者协同工作。
组件 RouterLink 和 RouterView 都是全局注册的,因此它们不需要在组件模板中导入。
<!– 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 模式
createWebHashHistory,URL 带有一个一个哈希字符(#),不需要在服务器层面上进行任何特殊处理。不过,它在 SEO 中确实有不好的影响。如果你担心这个问题,可以使用 HTML5 模式。
import { createRouter, createWebHashHistory } from \”vue-router\”;
const router = createRouter({
history: createWebHashHistory(),
routes: [
//…
],
});
HTML5 模式
createWebHistory,URL很漂亮,不带#,需要在服务器上添加一个简单的回退路由,否则可能会得到一个 404 错误。
import { createRouter, createWebHistory } from \”vue-router\”;
const router = createRouter({
history: createWebHistory(),
routes: [
//…
],
});
Memory 模式
createMemoryHistory,Node 环境或者 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 params 和 query 的区别
引入方式:
query:通常使用 path 来引入,即 URL 中直接拼接查询参数。params:需要使用 name 来引入路由,如果尝试使用 path 引入并传递 params,则参数会被忽略,并可能出现警告。 参数位置:
query:参数位于 URL 的查询字符串中,即在“?”之后,多个参数之间使用“&”分隔。params:参数是路由的一部分,通常位于 URL 的路径中,以冒号(:)开始,用于标识相对应的值。 参数显示:
query:参数会显示在浏览器的地址栏中,类似于 AJAX 中的 GET 请求。params:参数不会显示在浏览器的地址栏中,类似于 POST 请求。 刷新影响:
query:刷新页面后,通过 query 传递的参数仍然会保留在地址栏中,因此值不会丢失。params:刷新页面后,通过 params 传递的参数可能会丢失,因为它们不是 URL 的一部分。 获取方式:
query:可以通过this.$route.query来获取通过 query 传递的参数。params:可以通过this.$route.params来获取通过 params 传递的参数。 使用场景:
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
useRoute 和 useRouter 是 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,但它返回的是路由的实例,而不是当前路由的路由对象。返回值:这个路由实例包含了路由的许多方法,如导航(push、replace)、编程式导航等。适用场景:适用于那些需要进行动态路由操作的场景,如编程式导航、监听路由变化等。示例:
<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\” }; // 否则,重定向到登录页面
}
},
},
];
**注意:**不建议在一个路由配置中同时设置 component 和 redirect 属性,因为这两个属性在功能上是冲突的。
比如这样子写是不对的
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”是指将父组件的未被子组件声明的为 props 或 emits 的 属性 或者 v-on 事件监听器传递到子组件的根元素上。最常见的例子就是 class、style 和 id。这在创建可重用组件时特别有用,因为它允许父组件动态地设置子组件根元素的属性。
<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 provide 和 inject
provide 和 inject 是 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 接受一个工厂函数作为参数,该工厂函数接受两个参数:track 和 trigger。track 用于收集依赖,trigger 用于触发响应。工厂函数需要返回一个具有 get 和 set 方法的对象。
例如:
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-if 和 v-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.exe 和 Software2.exe 是你要打开的软件的路径,你需要将它们替换为实际的软件路径。
最后两行是 VS Code 的路径和你想要使用VS Code打开的文件夹的路径。
路径怎么找?右键文件夹或者软件,点击属性(R),打开文件所在的位置(F),复制路径后 CV 就可以了。提醒一下,启动程序一定要带上,比如上面的 .exe 文件。全程无毒副作用,使用简单,方便快捷,值得尝试。
不想学了,想一键关闭这些文件或者软件,实现比较麻烦,建议拔电源线或者关机。
备忘录:
1 v-if v-for 的优先级问题
#以上关于10分钟入门Vue3的相关内容来源网络仅供参考,相关信息请以官方公告为准!
原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/91306.html