本文小编为大家详细介绍“vue3基础知识实例分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“vue3基础知识实例分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
Composition Api (最重要的新特性)
组件通信
生命周期
自定义Hook
插槽
v-model的更改
更加纯粹的Tree-shaking
配合状态管理的Pinia
配合升级的vue-router 4.x
配合升级的打包工具vite
配合TS在项目中自由使用
优点
使用vue3
最大的优势个人认为倒不是它的Api,而是配合使用的vite
打包工具,特别是大型项目本地启动要比当前的webpack5
要快至少2倍
以上(项目中测试过)
比起vue 2.x
,Composition Api
的优势要明显的多,如果习惯了setup语法糖
的写法,你会发现爽的飞起,很多之前在vue 2.x
中大量重复逻辑不存在了
底层通过Proxy
来实现双向绑定,性能上提升了很多
对TypeScript
支持度更好,可以很愉快的在项目中使用TypeScript
缺点
如果还有IE情节
的公司,那vue3
确实不太适合,因为vue3
已经抛弃了对IE11
的支持,再说了 微软人家自己都不打算维护IE
了,兄弟们,放弃IE
拥抱chrome
吧!
Composition Api
的写法需要花一点点时间来适应,毕竟学习新语法还是需要成本的
通过CDN
<script src="https://unpkg.com/vue@next"></script>
npm
# 最新稳定版 npm install vue@next npm install -D @vue/compiler-sfc
如果你是从Vue 2.x
升级的,请注意 @vue/compiler-sfc
替换掉了 vue-template-compiler
vue-cli
npm install -g @vue/cli vue upgrade --next
vite
npm init vite@latest <project-name> -- --template vue cd <project-name> npm install npm run dev
推荐使用第4种方式,直接使用官方推荐最新的vite打包工具,直接初始化项目。
setup
是vue3
提出的一个非常重要的选项,也是Composition Api
最为核心的语法之一。
setup
执行时机是在beforeCreate
之前执行的。
setup
返回的是一个对象,对象中的所有属性都是可以在template
中使用
setup
中不能使用this
setup
中注册生命周期onMounted
、watch
、computed
等,我们会在下边详细讲解
setup参数
props
context
<script> export default { setup (props, context) { return {} } } </script>
既然上边提到了setup
语法,那就有必要把setup
语法糖介绍一下,我们在实际的项目开发中在熟悉了setup语法的本质后,也推荐大家使用setup
语法糖来编写,这样也可以大大提升开发效率。
不需要像上述一样return
,只需要在<script setup>
中声明一下即可
任何在 <script setup>
声明的顶层的绑定 (包括声明的变量,函数声明,以及 import
引入的内容) 都可以在模板中直接使用
组件在语法糖中可以自动注册,无需再通过components
进行注册
<script setup> import {ref} from 'vue' let property = ref('这里是响应式属性'); // 这里我们引入了子组件SetUp.vue import SetUp from '@/components/SetUp.vue' </script>
ref
跟reactive
都是vue3
中用来做数据定义使用的,如同vue2
中在data
中做数据定义一样,示例代码如下:
<template> <h4>{{ state.count }}</h4> <h4>{{ num }}</h4> <el-button @click="handleAdd" type="primary">ref计算</el-button> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const num = ref(0) const state = reactive({ count: 1 }) function handleAdd() { state.count++; num.value += 2; } return { state, num, handleAdd } } } </script>
ref
跟reactive
的区别在哪呢?很多人分不清楚,网上有很多文章简单的定义为ref
负责处理基本数据类型的双向绑定,reactive
负责处理对象的双向绑定。其实,这样笔者会觉得给很多初学者带来很多误导,其实ref
也可以处理对象的双向绑定,就像下边这段代码一样。
<template> <el-button @click="handleAdd" type="primary">ref计算</el-button> <h4>{{ obj.count }}</h4> </template> <script> export default { setup() { // ref 对象双向绑定 const obj = ref({ count: 1 }) function handleAdd() { obj.value.count = obj.value.count + 1 } return { obj, handleAdd } } } </script>
watchEffect
当传入一个函数时,可以响应式的自动收集依赖,当依赖变更时重新运行该函数;
使用是需要配置flush: post
,否则依赖在监听时无法被立即更新
也可以使用stop
来立即停止对函数的监听
<template> <div ref="root">This is a root element</div> </template> <script> import {ref, watchEffect} from 'vue' export default { setup() { const root = ref(null) watchEffect(() => { console.log(`watchEffect监听:${root.value}`); }, { flush: 'post' }) return { root } }, } </script>
watch
watch
API 与选项式 APIthis.$watch
(以及相应的watch
选项) 完全等效。watch
需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
与 watchEffect
相比,watch
:
是一个返回任意值的getter
函数
是一个包装的对象,可以是ref
对象、也可以reactive
对象
可以同时监听多个数据源
监听是需要配置deep: true
,否则回调函数无法被触发
<template> <h4>监听单个数据源1:{{state1.count}}</h4> <button @click="handleWatchSingle1">watch监听测试1</button> <h4>监听单个数据源2:{{state2}}</h4> <button @click="handleWatchSingle2">watch监听测试2</button> <h4>监听复杂对象数据源:{{state3.player}}</h4> <button @click="handleWatchSingle3">watch监听测试3</button> </template> <script> import {ref, reactive, watch} from 'vue' export default { setup() { const state1 = reactive({ count: 1 }) const state2 = ref(0) const state3 = reactive({ player: { name: 'James', achievement: ['4次NBA常规赛mvp', '03年选秀状元', '4次NBA总冠军'] } }) watch(() => state1.count, (newVal, oldVal) => { console.log('watch监听reactive中的newVal:', newVal); console.log('watch监听reactive中的oldVal:', oldVal); }) watch(() => state2.value, (newVal, oldVal) => { console.log('watch监听ref中的newVal:', newVal); console.log('watch监听ref中的oldVal:', oldVal); }) watch(() => state3.player, (newVal, oldVal) => { console.log('watch监听复杂对象中的newVal:', newVal); console.log('watch监听复杂对象中的oldVal:', oldVal); }, { deep: true, // immediate: true }) // 同时监听多个值 // watch([() => state1.count, state2.value], ([newVal1, newVal2], [oldVal1, oldVal2]) => { // console.log('watch监听中的newVal:', newVal1, newVal2); // console.log('watch监听oldVal:', oldVal1, oldVal2); // }) function handleWatchSingle1() { state1.count++ } function handleWatchSingle2() { state2.value++ } function handleWatchSingle3() { state3.player = { name: 'Wade', achievement: ['3次NBA总冠军', '曾经的热火三巨头之一', '1次NBA总决赛mvp'] } } return { state1, state2, state3, handleWatchSingle1, handleWatchSingle2, handleWatchSingle3 } }, } </script>
接受一个 getter
函数,并根据getter
的返回值返回一个不可变的响应式 ref
对象。
接受一个具有 get
和 set
函数的对象,用来创建可写的 ref
对象
<template> <div > <h4>computedNum值为:{{computedNum}}</h4> <h4>computedNum2值为:{{computedNum}}</h4> <button @click="handleComputed">computed计算测试</button> </div> </template> <script> import { ref, computed } from 'vue' export default { setup() { const state = ref(1) const computedNum = computed(() => { return state.value + 1 }) console.log('computed缓存后的值:', computedNum.value); // 只可读属性,不可写,会抛出警告 Write operation failed: computed value is readonly function handleComputed() { computedNum.value++ } const computedNum2 = computed({ get: () => state.value + 2, set: val => { count.value = val - 0 } }) return { computedNum, computedNum2, handleComputed } }, } </script>
组件通信这块跟vue2的区别不大,我们就拿常用的props跟emit来讲解一下。
父级组件向子组件传递数据
子组件想父组件传递数据
需要通过emits
选项来定义组件可触发的事件
父组件
<template> <Children :msg1="msg1" :msg2="msg2" @childClick="handleClick" /> </template> <script> import {ref, reactive} from 'vue'; import Children from './children.vue' export default { setup() { const msg1 = ref('给子组件传递的消息1') const msg2 = reactive({ name: '给子组件传递的消息2' }) return { msg1, msg2 } }, methods: { handleClick(val) { console.log('接收子组件emit过来的数据:', val); } }, components: { Children } } </script>
子组件
<template> <div >props传递给子组件的消息:{{ msg1 }}</div> <button @click="$emit('childClick', 6666)" >向父组件emits事件</button> </template> <script> export default { props: ['msg1', 'msg2'], emits: ['childClick'], setup(props) { console.log('子组件接收父级组件传递过来的消息:', props); }, } </script>
子组件
<template> <slot name="title"></slot> </template>
父组件
<template slot="title"> <h3>周岭:《认知觉醒》</h3> <template>
vue3
插槽中提供了v-slot:name
写法,我们就拿作用域插槽来举例
子组件
我们定一个可循环的插槽content
<template> <!-- <slot name="title"></slot> --> <div v-for="(item, index ) in items" :key="index"> <slot :item="item" name="content"></slot> </div> </template> <script setup> import {ref} from 'vue'; const items = ref(['认知觉醒', '认知驱动']); </script>
父组件
父组件中可以有两种方式来引入子组件中的插槽,其一是通过v-slot:content="scopend"
的方式,其二是通过简写#content="{item}"
的方式
<template> <SlotChild> <!-- <template v-slot:content="scoped"> <div>{{ scoped.item }}</div> </template> --> <template #content="{item}"> <div>{{ item }}</div> </template> </SlotChild> </template> <script setup> import SlotChild from './SlotChild.vue' </script>
vue3的声明周期如果是使用选项性Api的话,原来的生命周期钩子可以照常使用,那如果选用vue3组合式Api的话,生命周期需要通过import引入的方式在setup中调用。下图是vue3跟vu2声明周期的区别
<template> <div id="test"> <h4>{{ counter }}</h4> <button @click="handleClick">声明周期测试</button> </div> </template> <script> import { ref, onMounted, onBeforeMount, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' export default { setup() { const counter = ref(0); console.log('....'); function handleClick() { counter.value += 1; } onBeforeMount(() => { console.log("组件挂载之前"); }); onMounted(() => { console.log("DOM挂载完成"); }); onBeforeUpdate(() => { console.log("DOM更新之前", document.getElementById("test").innerHTML); }); onUpdated(() => { console.log("DOM更新完成", document.getElementById("test").innerHTML); }); onBeforeUnmount(() => { console.log("实例卸载之前"); }); onUnmounted(() => { console.log("实例卸载之后"); }); return { counter, handleClick } }, } </script>
vue-router 3.x跟vue-router 4.x比起来写法上的区别
vue-router 3.x
// router/index.js import Vue from 'vue' import Router from 'vue-router' import routes from './routes' Vue.use(Router) const router = new Router({ routes }) export default router // main.js import Vue from 'vue' import router from './router' // ... new Vue({ el: '#app', router, components: { App }, template: '<App/>' })
vue-router 4.x
// router/index.js import { createRouter } from 'vue-router' import routes from './routes' const router = createRouter({ history: createWebHistory(), // history模式 routes }) // main.js import { createApp } from 'vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')
将new Router()
改成createRouter()
将mode: 'history'
改成 history: createWebHistory()
useRouter、useRoute
通过useRouter
进行路由跳转
<template> <div class="mg30"> <el-button @click="handleJump" type="primary">关于我们</el-button> </div> </template> <script setup> import { useRouter } from 'vue-router' const router = useRouter() const handleJump = (query) => { router.push({ name: "about", query: { id: 1 } }) } </script>
通过useRoute
来获取传递过来的id
<template> <div>关于我们</div> </template> <script setup> import { useRoute } from 'vue-router' const route = useRoute() console.log('id>>>', route.query.id); </script>
全局守卫
/router/index.js
详情页面meta中
添加登录标识needLogin
let routes = [ { path: '/detail', name: 'detail', component: () => import('@/views/detail.vue'), meta: { needLogin: true } } ]
main.js
添加守卫
import router from './router' // 全局路由守卫 router.beforeEach((to, from) => { if (to.meta.needLogin) { return { name: 'login' } } })
路由独享守卫
/router/index.js
let routes = [ { path: '/category/:id', name: 'category', component: () => import('@/views/category.vue'), beforeEnter: (to, from) => { // 如果不是正确的分类,跳转到NotFound的页面 console.log('id>>>>', to.params.id); if (!["0", "1", "2"].includes(to.params.id)) { return { name: "NotFound", // 这个是在地址栏保留输入的信息,否则地址栏会非常的丑 params: { pathMatch: to.path.split("/").slice(1) }, query: to.query, hash: to.hash, }; } } } ]
组件内部守卫
<template> <div>关于我们</div> </template> <script setup> import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' // 页面内部的路由守卫 onBeforeRouteLeave((to, from) => { const answer = window.confirm('是否确认离开') if (answer) { console.log('不离开'); return false } }) // 对于一个带有动态参数的路径 /category/:catId,在 /category/1 和 /category/2 之间跳转的时候, 会触发onBeforeRouteUpdate的路由钩子函数,在钩子函数中可以进行数据的更新。 onBeforeRouteUpdate((to, from) => { console.log('to>>>', to); console.log('from>>>', from); // if (to.params.id !== from.params.id) { // userData.value = await fetchUser(to.params.id) // } }) </script>
// vue-router 3 <keep-alive> <router-view /> </keep-alive> // vue-router 4 <router-view v-slot="{component}"> <keep-alive> <component :is="component" /> </keep-alive> </router-view>
跟vue2不同的是,vue3中提供了提供了很多不同的选择器方便我们在样式编写上更加的灵活多变。
类似于sass
语法中的v::deep
,不过vue3
中的样式自带深度作用域
<style scoped> .parent :deep(div) { margin-bottom: 10px; } </style> <template> <div class="parent"> <div class="set-up">:deep 深度作用域测试</div> </div> </template>
不用像vue2
一样写全局作用域时,需要单独开启一个style
标签,同时去掉scoped
属性;vue3
提供了一种便捷的写法,只需要使用global
属性传递你想全局修改的样式即可。
<template> <div>全局选择器测试</div> <p :class="$style.green">module样式测试</p> </template> <style scoped> :global(div) { color: red; } </style>
<style module>
标签会被编译为 CSS Modules
并且将生成的 CSS
类作为 $style
对象的键暴露给组件。
<template> <p :class="$style.green">module样式测试</p> </template> <style module> .green { color: green; } </style>
<template> <p :class="classes.blue">useCssModule样式测试</p> </template> <style module="classes"> .blue { color: blue; } </style>
<script> import { h, useCssModule } from 'vue' export default { setup() { const style = useCssModule() return () => h( 'div', { class: style.success }, 'Task complete!' ) } } </script> <style module> .success { color: #090; } </style>
对于TS,笔者认为小项目中也不必集成TS,反倒会提升项目的编译成本。那如果是大型项目的话,有必要尝试接入TS,一方面可以减少不必要的类型判断及文档注释,同时可以及早的发现错误,做静态类型检查时就可以及时的发现问题。另一方面,类、接口的使用更易于构建和维护组件;那么,对于初学者我们有必要对TS的一些基本用法做一下普及。
/** * @description: 基本的数据类型 * @return {*} boolean(布尔值)number(数值) Array<number> (泛型数组)Object (对象)null undefined */ let isDone: boolean = false; console.log('isDon', isDone); let num: number = 1; console.log('num', num); let str: string = '认知觉醒'; console.log('str', str); let arr: number[] = [1, 2, 3]; console.log('arr', arr); // 泛型数组 let arr2: Array<number> = [1, 2, 3] console.log('arr2', arr2); let obj: Object = { id: 1 } console.log('obj', obj); let u: undefined = undefined console.log('u', u); let n: null = null; console.log('n', n);
// 数字类型枚举与数字类型 enum CardSuit { Clubs, Diamonds, Hearts, Spades } console.log('CardSuit', CardSuit.Clubs); // 0 let col = CardSuit.Clubs; col = 0 // 安全有效的 console.log('col', col); // 0 // 数字类型枚举与字符串类型 enum Tristate { False, True, Unkonw } console.log('字符串', Tristate[0]); // 'False' console.log('number', Tristate['False']); // 0 console.log('字符串', Tristate[Tristate.False]); // 'False' // 字符串枚举 enum LogLevel { info = 'info', warn = 'warn', error = 'error' } console.log('LogLevel', LogLevel.info); // 'info'
/** * @description: 元祖 * @return {*} 允许数组各元素的类型不必相同 */ let x: [string, number, boolean]; x = ['hello', 10, true]; console.log('正确元祖', x); // ['hello', 10, true] // y = [10, 'hello', false] // console.log('错误的元祖', y);
/** * @description: 任意值 Any * @return {*} 表示任意类型, 通常用于不确定内容的类型,比如用户的输入或者是第三方库代码;实际项目中,此类型建议少用 */ let notSure: any = 4; notSure = 'maybe a string instead'; console.log('notSure', notSure); // 'maybe a string instead' notSure = true; console.log('notSure', notSure); // true
/** * @description: 空值 void * @return {*} 与any相反,通常用于函数,表示没有返回值 */ const voidFunc = (): void => { console.log('这个函数没有返回任何值'); // return msg; // 不能return } voidFunc()
/** * @description: 接口 interface * @return {*} 类型契约,跟我们平时与服务端接口要先定义字段是一个道理 */ interface Point { x: number y: number z?: number readonly l: number } const point: Point = { x: 10, y: 20, z: 30, l: 40 } console.log('point', point); const point2: Point = { x: '10', y: 20, z: 30 } // Error x应该是Number类型 const point3: Point = { x: 10, y: 20, z: 30 } // Error l字段也是必传 const point4: Point = { x: 10, y: 20, z: 30, l: 40, m: 50 } // Error m字段没有定义 const point5: Point = { x: 10, y: 20, l: 40 } // 正常 point5.l = 20; // Error l字段是只读类型,不能修改
/** * @description: 函数参数类型与返回值类型 * @return {*} */ function sum(a: number, b: number): number { return a + b; } console.log('sum', sum(2, 3)); // 5 // 配合interface使用 interface Point { x: number y: number } function sum2({x, y}: Point): number { return x + y; } console.log('sum2', sum2({x: 1, y: 2})); // 3
/** * @description: 泛型 * @return {*} 泛型的意义在于函数的重用性,设计原则希望组件不仅能够支持当前的数据类型,同时也支持未来的数据类型 * 语法:<T>(arg: T): T */ // 比如我们最初设计函数identity 入参为String function identity(arg: String) { return arg; } console.log(identity('hello')); // hello // 后来随着业务的迭代我们又需要支持 Number function identity2(arg: String) { return arg; } console.log(identity(2)); // Argument of type 'number' is not assignable to parameter of type 'String' // 那我们为什么不用any呢?使用any会导致丢失掉一些信息,我们无法确定要返回值到底是属于什么数据类型 const hello1: String = 'Hello vue3'; const hello2: Number = 666; function say<T>(arg: T): T { return arg; } console.log('泛型1:', say(hello1)); // Hello vue3 console.log('泛型2:', say(hello2)); // 666 // 泛型约束 // 我们使用同样的例子,加了一个console,但是很不幸运,报错了,因为泛型无法保证每种类型都有.length 属性 const hello3: String = 'Hello vue3'; function say2<T>(arg: T): T { console.log(arg.length); // Property 'length' does not exist on type 'T' return arg; } console.log('泛型3:', say2(hello3)); // Hello vue3 interface Lengthwise { length: number } function say3<T extends Lengthwise>(arg: T): T { console.log(arg.length); return arg; } console.log(say3(1)); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'. console.log(say3({ value: 'hello vue', length: 10 })); // '{ value: 'hello vue', length: 10 }'
interface foo { x: number } interface bar { b: string } type intersection = foo & bar const result: intersection = { x: 10, b: 'hello' } console.log('result', result);
/** * @description: 联合类型 * @return {*} 表示一个值可以为几种数据类型之一 */ type arg = string | number | boolean const foo = (arg: arg): any => { console.log('arg', arg); } foo(1) foo('1') foo(true)
/** * @description: 函数重载 * @return {*} 1个函数可以执行多项任务的能力 */ // add函数,它可以接收string类型的参数进行拼接,也可以接收number类型的参数进行相加 function add <T, U>(arg1: T, arg2: U) { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2 if (typeof arg1 === 'string' && typeof arg2 === 'string') { return arg1 + arg2 } else if (typeof arg1 === 'number' && typeof arg2 === 'number') { return arg1 + arg2 } } console.log('number类型相加', add(1, 2)); console.log('string类型拼接', add('1', '2'));
首先,你可以在初始化项目的时候就选择TS模板,直接将TS相关配置集成到项目中去。
当然,你也可以手动去配置TS
安装TS
npm i typescript
项目根目录新建tsconfig.json
文件,用于TS的编译基础文件
{ "compilerOptions": { "target": "esnext", "module": "esnext", "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] }
项目中使用
script标签中声明langg="ts",然后就可以愉快的使用TS的项目语法了,下边这段代码只是一些简单的示例。
<template> <div> <h3>标题:{{book.title}}</h3> <h3>作者:{{book.author}}</h3> <h3>出版日期:{{book.year}}</h3> <hr> <h4>{{allTitle}}</h4> <el-button @click="setTitle('我是传入的数据')" type="primary">设置数据</el-button> </div> </template> <script lang="ts"> import { defineComponent, ref, toRefs, reactive } from 'vue'; // 定义Book接口 interface Book { title: String author: String year?: Number, handleChangeName?(): void } export default defineComponent ({ data() { let book: Book = { title: 'vue3 typescript', author: "vue Team", year: 2020, } return { book } }, setup() { let year1 = ref<String | Number>('2022') console.log('year1', year1.value); // 第一种方式 // const book1: Book = reactive({ // name: year1.value, // desc: "vue3 进阶学习加油", // setNamechange(){ // this.name = "我是新设置的" // } // }); // // 第二种方式 // const book2 = reactive<Book>({ // name: "vue3--typeScript", // desc: "学习ts加油", // year: 2020, // }); // // 第三种方式 // const book3 = reactive({ // name: "vue3--typeScript-第三种方式", // desc: "ts类型第三种方式", // year: 2022, // }) as Book; return { // ...toRefs(book1), // book2, // book3, // year1, }; }, computed: { // 返回值类型为String allTitle(): String { return `欢迎语 : ${this.book.title}` } }, methods: { // 入参为String 返回空值 setTitle(arg: String): void { this.book.title = arg; this.book.year = 2022 this.book.author = '尤雨溪' } } }) </script>
读到这里,这篇“vue3基础知识实例分析”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。