Vue3基础
1. 简介
vue2优势:
- 长期稳定版(2016年9月发布,近140次版本更新迭代)
- 生态系统,文档系统完善
- 过往案例项目支撑
vue3优势:
- 更容易维护
- 组合式 API
- 更好的 TypeScript 支持
- 更快的速度
- 重写 diff 算法
- 模板编译优化
- 更高效的组件初始化
- 更小的体积
- 良好的 TreeShaking
- 按需引入
- 更优的数据响应式
- proxy
2. 使用create-vue创建项目
create-vue
是官方新的脚手架工具,底层由原 vue2 (vue-cli)的webpack
切换成了vite
(下一代前端工具链),为开发提供极速响应
- 前提环境条件:已安装16.0或者更高版本的
node.js
(node -v) - 创建一个vue应用:
npm init vue@latest
这一指令将会安装并执行create-vue
- Project name: vue3-dome
- Add TypeScript? No
- Add JSX Support? No
- Add Vue Router for single Page Application development? Yes
- Add Pinia for state management? Yes
- Add Vitest for Unit Testing? No
- Add Cypress for both Unit and End-to-End testing? No
- Add ESLint for code quality? Yes
- Add Prettier for code formatting? No
-
关键文件等变化:
- 配置文件:原
vue.config.js
文件变为vite.config.js
文件 - 入口文件:
// main.js // new Vue() 创建一个应用实例对象 import { cteateApp } from 'vue' import App from './App.vue' import './assets/main.css' // 1. 以App作为参数生成一个应用实例对象 // 2. 挂载到id为app的节点上(根目录下index.html文件中) createApp(App).mount('#app')
- 根组件App.vue
<!-- setup:相当于一个开关,容许在script书写组合式API --> <script setup>...</script> <!-- 不再要求唯一根元素包裹 --> <template>...</template> <style scoped></style>
- 配置文件:原
3. 组合式API - setup选项
3.1 setup选项的写法和执行时机
<script>
// setup执行时机在beforeCreate之前
export default {
setup() {},
beforeCreate() {}
}
</script>
3.2 setup选项中写代码的特点
vue3中不再推荐使用this
,如果在setup
中使用或者打印this
是获取不到的(undefined)
<script>
export default {
setup() {
// 数据
const msg = 'this is msg'
// 函数
const logMsg = () => {
console.log(msg)
}
return {
msg,
logMsg
}
}
}
</script>
<template>
<!-- 使用数据和方法 -->
{{ msg }}
<button @click="logMsg">to msg</button>
</template>
3.3 script setup 语法糖
为了编写方便,不用定义一个数据/函数就return一个,所以vue3提供了一个语法糖
原始复杂写法:
<script>
export default {
setup() {
// 数据
const msg = 'this is msg'
// 函数
const logMsg = () => {
console.log(msg)
}
return {
msg,
logMsg
}
}
}
</script>
语法糖写法:
<script setup>
// 数据
const msg = 'this is msg'
// 函数
const logMsg = () => {
console.log(msg)
}
</script>
4. 组合式API - reactive和ref函数
4.1 reactive()
作用:接收对象类型数据的参数传入并返回一个响应式的对象,普通对象不会实现视图更新,所以需要用reactive
函数
核心步骤:
<script setup>
// 1.导入
import { reactive } from 'vue'
// 2.执行函数 3.传入一个对象类型参数 变量接收
const state = reactive({count: 0})
// 事件函数
const setCount = () => {
state.count++
}
</script>
<template>
<div>
<button @click="setCount">{{ state.count }}</button>
</div>
</template>
4.2 ref()
作用:接收简单类型或者对象类型的数据传入并返回一个响应式的对象‘
核心步骤:
<script setup>
// 1.导入
import { ref } from 'vue'
// 2.执行函数 3.传入参数(简单类型/对象类型) 变量接收
const count = ref(0)
// 事件函数
const setCount = () => {
// 脚本区域修改ref产生的响应式对象数据,必须通过 .value 属性来操作
count.value++
}
</script>
<template>
<div>
<button @click="setCount">{{ count }}</button>
</div>
</template>
4.3 总结
- reactive和ref函数的共同作用是用函数调用的方式生成响应式数据
- reactive和ref的区别:
- reactive不能处理简单类型的数据
- ref参数类型支持更好,但是必须通过
.value
访问修改 - ref函数的内部实现依赖于reactive函数
- 在实际工作中使用ref函数更加灵活
5. 组合式API - computed
计算属性基本思想和vue2的完全一致,组合式API下的计算属性只是修改了写法
核心步骤:
<script setup>
// 1.导入
import { ref, computed } from 'vue'
// 原始响应式数组
const list = ref([1, 2, 3, 4, 5, 6, 7, 8])
// 2.执行函数,return计算之后的值,变量接收
const computedList = computed(() => {
return list.filter(item => item > 2)
})
</script>
<template>
<div>原始响应式数组 - {{ list }}</div>
<div>通过计算属性返回的数组 - {{ computedList }}</div>
</template>
计算属性中不应该有“副作用”(比如异步请求/修改dom),可交给watch
避免直接修改计算属性的值(计算属性应该是只读的)
6. 组合式API - watch
作用:侦听一个或多个数据的变化,数据变化时执行回调函数
两个额外参数:
- immediate(立即执行)
- deep(深度侦听)
6.1 基本使用
侦听单个数据
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const setCount = () => {
count.value++
}
// ref对象在watch中不需要加.value
watch(count, (newVal, oldVal) => {
console.log('count变化了', newVal, oldVal)
})
</script>
<template>
<div>
<button @click="setCount">{{count}}</button>
</div>
</template>
侦听多个数据:同时侦听多个响应式数据的变化,不管哪个数据变化都需要执行回调
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const changeCount = () => {
count.value++
}
const name = ref('zs')
const changeName = () => {
name.value = 'ls'
}
// watch侦听多个数据
watch(
[count, name],
(
[newCount, newName],
[oldCount, oldName]
) => {
console.log('count或name变化了', [newCount, newName], [oldCount, oldName])
}
)
</script>
<template>
<div>
<button @click="changeCount">修改count-{{ count }}</button>
</div>
<div>
<button @click="changeName">修改name-{{ name }}</button>
</div>
</template>
6.2 immediate 立即执行
说明:在侦听器创建时立即触发回调,响应式数据变化之后继续执行回调
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const setCount = () => {
count.value++
}
// 立即执行
watch(count, () => {
connsole.log('count发生了变化')
}, {
immediate: true
})
</script>
6.3 deep 深度侦听
默认机制:通过watch
监听的ref
对象默认是浅层侦听,直接修改嵌套的对象属性是不会触发回调执行的,需要开启deep
选项
<script setup>
import { ref, watch } from 'vue'
const data = ref({count: 0})
const changeData = () => {
// 直接修改属性不会触发侦听回调
data.value.count++
}
watch(data, (newVal, oldVal) => {
console.log('data.count改变了', newVal, oldVal)
},{
deep: true // 加上deep可触发侦听对象属性
})
</script>
<template>
<div>
<button @click="changeData">{{ data.count }}</button>
</div>
</template>
注意:deep
存在性能损耗,尽量不开启deep
6.4 精准侦听
在不开启deep
的前提下,精准侦听对象属性,只有指定属性变化时才执行侦听回调
<script setup>
import { ref, watch } from 'vue'
const info = ref({name: 'zs', age: 18})
const changeInfo = () => {
info.value.age++
}
watch(
() => info.value.age,
(newVal, oldVal) => {
console.log('age改变了', newVal, oldVal)
}
)
</script>
<template>
<div>
<button @click="changeInfo">{{info.name}} - {{info.age}}</button>
</div>
</template>
7. 组合式API - 生命周期函数
vue3(选项式API)与(组合式API)生命周期函数区别
选项式API | 组合式API | 说明 |
---|---|---|
beforeCreate/created | setup | 创建 |
beforeMount | onBeforeMount | 挂载之前 |
mounted | onMounted | 挂在成功 |
beforeUpdate | onBeforeUpdate | 更新之前 |
updated | onUpdated | 更新成功 |
beforeUnmount | onBeforeUnmount | 卸载之前 |
unmounted | onUnmounted | 卸载成功 |
生命周期函数是可以执行多次的,多次执行时传入的回调会在时机成熟时依次执行
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
console.log('组件挂载完毕mounted执行了1')
})
onMounted(() => {
console.log('组件挂载完毕mounted执行了3')
})
onMounted(() => {
console.log('组件挂载完毕mounted执行了2')
})
// 控制台依次打印 ...1 ...3 ...2
</script>
8. 组合式API - 父子通信
8.1 父传子 - defineProps({属性名: 类型})
vue2中基本思想:
- 父组件中给子组件绑定属性
- 子组件内部通过props选项接收
vue3中具体操作:
<!-- 父组件 -->
<script setup>
// setup语法糖下局部组件无需注册直接可以使用
import sonComVue from './son-com.vue'
const count = 18
</script>
<template>
<div>我是父组件</div>
<!-- 1.绑定属性 msg -->
<sonComVue msg="this is msg" :count="count"></sonComVue>
</template>
<!-- 子组件 -->
<script setup>
// 2.通过 defineProps宏函数接收子组件传递的数据
const props = defineProps({
msg: String,
count: Number
})
console.log(props) // {msg: 'this is msg', count: 18}
</script>
<template>
<div>我是子组件</div>
<div>父组件传入的数据-{{msg}}-{{count}}</div>
</template>
8.2 子传父 - defineEmits([事件名称])
vue2中基本思想:
- 父组件中给子组件标签通过@绑定事件
- 子组件内部通过
$emit
方法触发事件
vue3中具体操作:
<!-- 父组件 -->
<script setup>
// 引入子组件
import sonComVue from './son-com.vue'
const getMsg = (msg) => {
console.log(msg)
}
</script>
<template>
<div>我是父组件</div>
<!-- 1.绑定自定义事件 -->
<sonComVue @getMsg="getMsg"></sonComVue>
</template>
<!-- 子组件 -->
<script setup>
// 2.通过defineEmits宏函数生成emit方法
const emit = defineEmits(['getMsg'])
const setMsg = () => {
// 3.触发自定义事件并传递参数
emit('getMsg', 'this is son msg')
}
</script>
<template>
<div>我是子组件</div>
<button @click="setMsg">触发自定义事件</button>
</template>
9. 组合式API - 模板引用
9.1 概念
通过ref标识获得真实的dom对象或者组件实例对象
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import TestCom from './test-com.vue'
// 1.调用ref函数得到ref对象
const h1Ref = ref(null)
const testComRef = ref(null)
// 组件挂载完成后才能获取
onMounted(() => {
console.log(h1Ref.value) // <h1 ref="h1Ref">我是dom标签h1</h1>
console.log(testComRef.value)
})
</script>
<template>
<!-- 2.通过ref标识绑定ref对象 -->
<h1 ref="h1Ref">我是dom标签h1</h1>
<TestCom ref="testComRef"/>
</template>
9.2 defineExpose() - 暴露组件内部的属性和方法
默认情况下在<script setup>
语法糖下组件内部的属性和方法是不开放给父组件访问的,可以通过defineExpose
编译宏指定哪些属性和方法允许访问
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import TestCom from './test-com.vue'
const testComRef = ref(null)
onMounted(() => {
console.log(testComRef.value)
})
</script>
<template>
<TestCom ref="testComRef"/>
</template>
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const name = ref('test name')
const setName = () => {
name.value = 'test new name'
}
defineExpose({
name,
setName
})
</script>
<template>
<div>我是test组件</div>
</template>
10. 组合式API - provide和inject
作用和场景:顶层组件向任意的底层组件传递数据和方法,实现跨层组件通信
10.1 跨层传递数据
- 顶层组件通过
provide
函数提供数据 - 底层组件通过
inject
函数获取数据
<!-- 顶层组件 -->
<script setup>
import RoomMsgItem from './room-msg-item.vue'
// 组件嵌套关系
// RoomPage -> RoomMsgItem -> RoomMsgComment
import { provide, ref } from 'vue'
// 1.顶层组件传递普通数据
provide('data-key', 'this is room data')
// 1.顶层组件传递响应式数据
const count = ref(0)
provide('count-key', count)
</script>
<template>
<div>
<p>顶层组件</p>
<RoomMsgItem />
</div>
</template>
<!-- 底层组件 -->
<script setup>
import { inject } from 'vue'
// 2.底层组件接收普通数据
const roomData = inject('data-key')
// 2.底层组件接收响应式数据
const countData = inject('count-key')
</script>
<template>
<div>
<p>底层组件</p>
<p>来自顶层组件中的数据:{{ roomData }}</p>
<p>来自顶层组件的响应式数据:{{ countData }}</p>
</div>
</template>
10.2 跨层传递方法
顶层组件可以向底层组件传递方法,底层组件调用方法修改顶层组件中的数据
谁的数据谁负责修改
<!-- 顶层组件 -->
<script setup>
import RoomMsgItem from './room-msg-item.vue'
// 组件嵌套关系
// RoomPage -> RoomMsgItem -> RoomMsgComment
import { provide, ref } from 'vue'
const count = ref(0)
provide('count-key', count)
// 1.传递方法
const setCount = () => {
count.value++
}
provide('setCount-key', setCount)
</script>
<template>
<div>
<p>顶层组件</p>
<RoomMsgItem />
</div>
</template>
<!-- 底层组件 -->
<script setup>
import { inject } from 'vue'
const countData = inject('count-key')
// 2.接收方法
const setCount = inject('setCount-key')
</script>
<template>
<div>
<p>底层组件</p>
<p>来自顶层组件的响应式数据:{{ countData }}</p>
<button @click="setCount">修改顶层组件的数据</button>
</div>
</template>
11. Pinia状态管理库
Pinia
是 vue 的专属的最新状态管理库,是vuex
状态管理工具的替代品
- 提供了更加简单的API(去掉了mutation)
- 提供符合组合式风格的API(和vue3新语法统一)
- 去掉了 modules 的概念,每个 store 都是一个独立的模块
- 搭配
Typescript
一起使用提供可靠的类型推断
11.1 action
// counter.js
// 导入一个方法 defineStore
import { defineStore } from 'pinia'
import { ref } from 'vue'
import axios from 'axios'
export const useCounterStore = defineStore('counter', () => {
// 定义数据(state)
const count = ref(0)
// 定义修改数据的方法(同步action)
const increment = () => {
count.value++
}
// 异步action
const getListAPI = 'http://XXX/list'
const list = ref([])
const getList = async() => {
const res = await axion.get(getListAPI)
list.value = res.data.data
}
// 以对象的方式return供组件使用
return {
count,
increment,
getList
}
})
action
中实现异步和组件中定义数据和方法的风格完全一致
<script setup>
// 1.导入useCounterStore方法
import { useCounterStore } from '@/stores/counter'
// 2.执行方法得到store实例对象
const counterStore = useCounterStore()
onMounted(() => {
counterStore.getList()
console.log('异步获取的数据:', counterStore.list)
})
</script>
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
</template>
11.2 getters
Pinia
中的getters
直接使用computed
函数进行模拟
// counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => {
count.value++
}
// getter定义
const doubleCount = computed(() => {
count.value*2
})
return {
count,
increment,
doubleCount
}
})
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<template>
<button @click="counterStore.increment">{{ counterStore.count }}</button>
<p>getter数据变化-{{counterStore.doubleCount}}</p>
</template>
11.3 storeToRefs
使用stroeToRefs
函数可以辅助保持数据(store+getter)的响应式结构
storeToRefs
函数只负责数据的解构(state/getter),方法无需storeToRefs
函数包裹,可直接结构赋值
若直接对数据进行解构赋值,会导致响应式丢失,视图不再更新
// counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => {
count.value++
}
// getter定义
const doubleCount = computed(() => {
count.value*2
})
return {
count,
increment,
doubleCount
}
})
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
// 保持响应式更新
const { count, increment } = storeToRefs(useCounterStore())
const { doubleCount } = useCounterStore()
</script>
<template>
<button @click="increment">{{ count }}</button>
<p>getter数据变化-{{ doubleCount }}</p>
</template>