Vue3 新增方法:
1. 创建Vue实例(main.js)
Vue2中,我们使用 Vue 这个构造函数,创建 Vue实例,而在 Vue3中,新增了 createApp()方法,用于创建应用实例。语法:
// 引入的不再是Vue构造函数,而是一个名为 createApp 的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例,并挂在到根元素
createApp(App).mount('#app')
2. 组件中的变化(template)
在Vue2中,template 标签需要有一个根标签,而这样做会产生一些不必要的嵌套。在Vue3中,template 标签中可以不使用根标签,因为Vue3中,会默认用<fragment></fragment>标签将多个标签进行包裹,fragment 标签表示文档碎片,是一个虚拟Dom,不会渲染为真实的 Dom 元素。
3. 组合式 API 与 setup() 函数
何为组合式API:
在Vue2 中,要写一个功能,我们通常会:
1)在 data 中,声明数据;
2)在 methods 中定义方法;
3)以及在 watch 和 computed 中进行监听数据
以及。。。
这样做会使我们的代码看起来,东一块,西一块,看起来非常不优雅。
而在 Vue3中,我们可以使用组合式API,将需要的API按需引入,并在setup() 函数中进行使用,我们可以像正常写 js 那样,进行书写,同一个功能的代码都会放在一起,逻辑非常清晰,也不需要频繁的使用 this.xxx 。
setup() 函数:
1). setup是 Vue3 新增的一个钩子函数,他会在 beforeCreate 之前调用。
2). setup 默认接收两个参数:
props: 表示父组件传递的参数对象
context 表示组件上下文对象,主要有这几个属性:
attrs 组件外部传递过来的值,但没有在props中声明,相当于 Vue2 的 this.$attrs;
emit 父组件传递的函数,相当于 this.$emit;
slots 父组件传递的插槽,相当于 this.$slots。
// 父组件
<template>
<h1>父组件</h1>
<h2 v-show="visible">这是父组件身上的</h2>
<Demo :msg="person.msg" @hello="change" :name="person.name" v-model:visible="visible">
<template v-slot:test>
<div>这是插槽</div>
</template>
</Demo>
</template>
<script>
import {ref,reactive} from 'vue'
import Demo from './components/Demo'
export default {
name: 'App',
components:{
Demo
},
setup(){
const visible = ref(true)
const person = reactive({
name: '张三',
msg: '雷猴啊'
})
function change(){
alert('你好,我是父组件身上的函数')
}
return {
visible,
person,
change
}
}
}
</script>
// 子组件
<template>
<h2>姓名:{{ name }}</h2>
<button @click="test">点击触发父组件传递过来的Hello事件</button>
<button @click="updateVisible">点击修改父组件身上的visible</button>
<slot name="test"></slot>
</template>
<script>
export default {
props: {
name: String,
msg: String,
visible: Boolean
},
emits: {
hello: Function
},
setup(props, context) {
console.log(props);
console.log(context);
console.log(context.slots);
function test(){
context.emit('hello')
}
function updateVisible(){
context.emit('update:visible', !props.visible)
}
return {
test,
updateVisible
}
}
}
</script>
4. 响应式数据
1). ref 引用对象
引入:import { ref } form 'vue'
声明:构造一个 ref 引用对象(返回一个 refImpl 对象):
let name = ref('张三') // (基本类型)
let job = ref({type: '前端', salary: '20k'}) // (引用类型)
// 修改 引用对象 值 需要 .value
name.value = '李四' //(基本类型)
job.value.type = 'UI' // (引用类型)
ref 构造基本类型值,底层使用的是 defineProperty 实现响应式 ;
构造引用类型值,借用了 reactive函数, 底层使用 proxy 实现响应式。
2). reactive
引入:import { reactive } form 'vue'
声明 一个 reactive 代理对象(proxy 实例):
const person = reactive({
name: '张三',
age: 18,
job: {
jobType: '前端攻城狮',
salary: '30k',
a: {
b: {
c: 666
}
}
},
hobby: ['吃','喝','嫖','赌']
})
// 修改 reactive 代理对象的值
person.name = '法外狂徒'
person.age = 20
person.job.jobType = '全干攻城狮'
person.job.salary = '50k'
person.job.a.b.c = 777
person.hobby[2] = '学习' // reactive 可以实现对数组元素的响应式
总结:
如果源数据是基本类型,则使用 ref 进行声明,如果是引用类型,使用 reactive 进行声明;
ref 构造基本类型值,底层使用的是 defineProperty 实现响应式 ;构造引用类型值,借用了 reactive函数, 底层使用 proxy 实现响应式。
5. 计算属性- computed
引入:import {reactive, computed} from 'vue'
使用:
// 简写方式(传入一个方法,计算属性只读时使用):
person.fullName = computed(() => {
return person.firstName + '-' + person.lastName
})
// 完整写法(传入一个对象,计算属性需要修改时使用):
person.fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
let nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
6. 监听属性 - watch
引入:import {ref, watch} from 'vue'
用法:
watch是一个函数,里面有三个参数:(监听的属性名, 回调函数(newValue, oldValue), 配置项)。
// 情况一: 监听一个 ref 响应式数据 immediate: true 表示数据加载就监听一次
watch(sum, (newValue, oldValue) => {
console.log('sum的值变化了', newValue, oldValue);
}, { immediate: true })
// 情况二: 监听多个 ref 响应式数据(需要传入一个数组,newValue和oldValue 也是数组)
watch([sum, msg], (newValue, oldValue) => {
console.log('sum/msg变化了', newValue, oldValue);
}, { immediate: true })
// 情况三:监听一个 reactive 响应式数据
/*
注意:
1. 此方式中,无法获取正确的 oldValue,newValue 和 oldValue 的值相同
2. 强制开启了深度监视 (deep 配置无效)
*/
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue);
})
// 情况四: 监听reactive 的某一个属性
watch(() => person.age, (newValue, oldValue) => {
console.log('person的age变化了', newValue, oldValue);
})
// 情况五: 监听reactive 的某些属性
watch([() => person.age, ()=>person.name], (newValue, oldValue) => {
console.log('person的age/name变化了', newValue, oldValue);
})
// 特殊情况: 监听reactive 的一个对象
watch(() => person.job, (newValue, oldValue) => {
console.log('person的job变化了', newValue, oldValue);
}, {deep: true})
总结:
当监听 ref 定义的数据时,且为基本类型,可以正确监听;
当监听 reactive 定义的数据时:
- 监听整个 reactive 定义的数据
默认开启深度监听(deep 不生效)
无法获取正确的 oldValue
- 监听 reactive 中的某个属性
- 该属性为 基本类型:可以正确监听
- 该属性为 引用类型:oldValue无法正确获取,deep 生效
注意:
如果监听 ref 定义的 引用类型 的响应式数据,需要在第一个参数后加 .value或者 添加选项 deep:true,因为 ref 定义 引用类型时,默认会调用reactive, 而 ref 返回的是一个 RefImpl 对象, .value 是一个 Proxy 对象。
7. watchEffect 函数
引入: import {ref, watchEffect, reactive} from 'vue'
使用:
// watchEffect 返回值是一个函数,作用是停止监听
const stop = watchEffect((before) => {
// before是一个函数,它会在执行副作用函数之前调用
// 先输出before 后输出 watchEffect配置的回调执行了
const a = sum.value
const b = person.job.job1.salary
console.log('watchEffect配置的回调执行了',a,b);
before(() => {
console.log('before')
})
})
// 停止监听
const stopWatch = () => stop()
总结:
watchEffect 不需要指明监听哪个属性,回调函数中引入了哪个属性,就监听哪个;
watchEffect 接收一个参数,这个参数是一个函数,会在副作用函数之前调用;
watchEffect 默认开启了 immediate;
watchEffect 返回一个函数,可以调用这个函数来停止监听;
watchEffect 与 computed 区别:
- computed 需要写返回值,watchEffect 不需要写返回值
8. 生命周期
Vue3 与 Vue2 的生命周期类似:
组合式API:
import { setup, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
使用:
onBeforeMount(() => {}) 传入一个函数
选项式API:
beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted
使用:created(){}
总结:
Vue2 中的 Destroy 变成了 unmount。
9. hooks
在了解了组合式API之后,我们可以将组件中,共有的逻辑,利用 组合式 API 分离出来,自定义为hooks(其实就是一个js方法),在需要的地方引入,此方式类似于 Vue2 中的 mixin。
示例:
// 组件内部
<template>
<h2>当前求和为:{{ sum }}</h2>
<button @click="sum++">点我加一</button>
<hr>
<h2>当前点击的鼠标坐标为:{x:{{ point.x }},y:{{ point.y }}}</h2>
</template>
<script>
import {ref} from 'vue'
// 引入自定义 hooks
import usePoint from '../hooks/usePoint'
export default {
setup() {
const sum = ref(0)
let point = usePoint()
return {
sum,
point
}
},
}
</script>
// 自定义 hooks usePoint.js
import {reactive, onMounted, onBeforeUnmount} from 'vue'
export default function (){
// 数据
const point = reactive({
x:0,
y:0
})
// 方法
function savePoint(event){
console.log({x:event.pageX,y:event.pageY});
point.x = event.pageX
point.y = event.pageY
}
// 钩子
onMounted(()=>{
window.addEventListener('click', savePoint)
})
onBeforeUnmount(()=>{
window.removeEventListener('click', savePoint)
})
return point
}
上方代码,定义了一个点击页面,输出鼠标坐标的 Hooks。
10. toRef 与 toRefs
引入: import {ref, toRef, toRefs} from 'vue'
toRef :
作用:创建一个 ref 对象,其 value值指向另一个对象中的某个属性(将一个对象的属性转为ref对象)
语法:
let name2 = toRef(person, 'name')
toRefs :
作用:与 toRef 作用类似,但是可以将一个对象的所有属性都变成 ref 对象。
语法:
const x = toRefs(person)
应用:将响应式对象中的某个属性,单独提供给外部使用。
11. shallowReactive 与 shallowRef
引入: import {ref, shallowReactive, shallowRef} from 'vue'
shallowReactive:
只对 对象最外层 属性做响应式处理
shallowRef:
只处理基本数据类型的响应式,不进行对象的响应式处理
语法:
// 对象只有第一层属性是响应式
let person = shallowReactive({a:1,b:2,c:{d:3})
应用:
如果一个对象只是最外层的属性发生变化 ===> shallowReactive
如果一个对象不会修改其中的属性,而是生成新的对象来替换 ===> shallowRef
12. readonly 与 shallowReadonly
引入: import {ref, readonly, shallowReadonly} from 'vue'
readonly: 将响应式数据变为,深只读(所有属性都为只读属性)
shallowReadonly: 将相应是数据变为,浅只读(即最外层属性为只读)
语法: 都是函数,传入一个响应式对象。
应用:不希望数据被修改时使用
13. toRaw 与 markRaw
引入: import {ref, toRaw, markRaw} from 'vue'
toRaw:
作用: 将一个 reactive 生成的 响应式对象 转为 普通对象
用法:
- const p = toRaw(person)
使用场景: 用于读取响应式对象对应的普通对象,对这个对象的所有操作,不会引起页面更新
markRaw:
作用: 标记一个对象,使其不变成响应式对象
用法:
- person.car = markRaw(car)
应用: 渲染具有不可变数据源的大列表时,跳过响应式转换,提高性能。
14. customRef 自定义ref
引入:
- import {customRef} from 'vue'
语法:
customRef 是一个函数,需要传入一个参数:
这个参数也是一个函数 (track, trigger) => { get(){}, set(){} }
此函数有两个参数:
- track:表示 追踪,通知Vue追踪 value 的变化(get() 中使用)
- trigger:表示 触发,通知Vue重新解析模板(set()中使用)
此函数需要返回一个对象,对象中有 get(), set() 两个方法。
实例:
<template>
<h1>app</h1>
<input type="text" v-model="keyWord" >
<h3>{{ keyWord }}</h3>
</template>
<script>
import {customRef} from 'vue'
export default {
name: 'App',
setup(){
// 自定义 ref ---> myRef
function myRef(value, delay){
return customRef((track, trigger) => {
let timer
return {
get(){
console.log('myRef中的数据被读取了', value);
track() // 通知 Vue 追踪 value 的变化
return value
},
set(newValue){
if(timer) clearTimeout(timer)
console.log('myRef中的数据被修改为', newValue);
timer = setTimeout(()=>{
value = newValue
trigger() // 通知 Vue 去重新解析模板
}, delay)
}
}
})
}
// let keyWord = ref('hello')
let keyWord = myRef('hello', 500)
return {
keyWord
}
}
}
</script>
上面代码中定义了一个 myRef的自定义 ref,属性被修改时,延迟500毫秒再显示到页面上。
15. provide 与 inject
作用:用于父组件与后代组件通信,传递数据
引入:
import {provide, inject} from 'vue'
语法:
父组件中提供数据:
provide('数据的引用名', 数据)
后代组件中注入数据:
inject('数据的引用名')
代码:
// 父组件:
let car = reactive({
name: '保时捷911',
price: '99w'
})
// 为后代组件提供数据
provide('car', car)
// 后代组件:
let car = inject('car')
16. 响应式数据类型判断方法
Vue3 中,内置了几个判断响应式数据类型的方法:
isRef():
判断数据是否为 ref 类型的响应式数据
isReactive():
判断数据是否为 reactive 类型的响应式数据
isProxy():
判断数据是否为 proxy 类型的响应式数据
isReadonly():
判断数据是否为 readonly 类型的响应式数据
Vue3 内置组件
Teleport 传送组件
作用:将组件传送到想要的元素下
语法:
<Teleport to="#test" :disabled="false"></Teleport>
to: 可以是选择器,也可以是body
disabled: 用于控制是否传送,true 表示不传送到指定位置,false 表示传送到指定位置。默认值为true。
实例:
<template>
<div>
<button @click="isShow=true">弹窗</button>
<Teleport to="body">
<!-- <Teleport to="#test"> -->
<div class="mask" v-if="isShow">
<div class="dialog" >
<h3>我是一个弹窗</h3>
<h4>一些内容</h4>
<h4>一些内容</h4>
<h4>一些内容</h4>
<button @click="isShow=false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup(){
let isShow = ref(false)
return {isShow}
}
}
</script>
上方代码,写了一个弹窗组件,并将组件传送到body身上。
2. Suspense 组件 和 defineAsyncComponent 异步组件
defineAsyncComponent:
作用: 用于组件的异步引入
语法:
import { defineAsyncComponent } from 'vue';
const Son = defineAsyncComponent(() => import('./components/Son.vue')
Suspense:
作用: 在等待异步组件加载时,额外渲染一些内容,比如加载loading,提高用户体验
语法:
<Suspense>
<template v-slot:default>
<Son></Son>
</template>
<template v-slot:fallback>
<h2>Loading......</h2>
</template>
</Suspense>
Suspense 组件有两个插槽
default,表示加载完成后,展示的组件;
fallback,表示组件加载时,展示的组件。
实例:
// 父组件
<template>
<div class="grand">
<h3>APP组件</h3>
<Suspense>
<template v-slot:default>
<Son></Son>
</template>
<template #fallback>
<h2>Loading......</h2>
</template>
</Suspense>
</div>
</template>
<script>
// import Son from './components/Son.vue' // 静态引入
// import {reactive,ref, toRefs} from 'vue'
import { defineAsyncComponent } from 'vue';
const Son = defineAsyncComponent(() => import('./components/Son.vue')) // 动态引入
export default {
name: 'App',
components: {
Son
},
setup(){
return {
}
}
}
</script>
<template>
<div class="son">
<h3>我是子组件</h3>
{{ sum }}
</div>
</template>
<script>
import { ref } from 'vue';
export default {
components: {
},
async setup(){
let sum = ref(0)
let p = new Promise((resolve) => {
setTimeout(()=>{
resolve({sum})
},1000)
})
return await p
}
}
</script>
默认情况下,setup函数返回的是一个普通对象,但在异步组件中,setup函数可以是一个异步函数,返回一个promise对象。
异步组件的另一种引入方式(类似于Vue2中的异步引入):
<script lang="ts">
export default{
components:{
'Demo': ()=> import('./components/Demo.vue')
}
}
</script>
为什么要用异步组件?
因为在项目打包时(npm run build),如果是静态引入,我们的组件会被打包为一个js文件,如果组件很多,这个js文件体积就会变得很大,在第一次加载时就比较慢。而动态引入时,我们的组件就会分别打包为一个个的js,加载速度就会快一点。
3. 递归组件
递归组件:一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue 的组件可以在其模板中用 <FooBar/> 引用它自己。
以下例子为一个树形组件:
父组件:
<template>
<h1>树</h1>
<Tree :data="data"/>
</template>
<script setup>
import { reactive } from 'vue'
import Tree from './components/Tree'
let data = reactive([
{
name: '1-1',
checked: false,
children: [
{
name: '1-1-1',
checked: false,
children: [
{
name: '1-1-1-1',
checked: true
}
]
}
]
},
{
name: '1-2',
checked: false
},
{
name: '1-3',
checked: true,
children: [
{
name: '1-3-1',
checked: true,
}
]
}
])
</script>
子组件:
<template>
<div @click.stop="onClick(item, $event)" class="tree" v-for="item in data" :key="item.name">
<input type="checkbox" :checked="item.checked">
<span class="color">{{ item.name }}</span>
<TreeItem v-if="item.children" :data="item.children" />
</div>
</template>
<script setup>
import {defineProps} from 'vue'
defineProps({
data: {
type: Array,
default: null
}
})
function onClick(item, e){
console.log(item);
console.log(e);
}
</script>
<!-- 递归组件中不想使用 Tree 表示自己,可以使用这种方式来重命名 -->
<script>
export default{
name:"TreeItem"
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.color{
color: transparent;
background: linear-gradient(81.02deg, rgb(250, 85, 96) -23.49%, rgb(177, 75, 244) 45.66%, rgb(77, 145, 255) 114.8%);
background-clip: text;
}
.tree{
margin-left: 20px;
}
</style>
4. KeepAlive 缓存组件
用于缓存组件实例。
include: 哪些组件需要缓存;
exclude: 哪些组件不需要缓存;
max : 缓存组件的最大个数;
使用 include 和 exclude时需要在组件中声明 name 选项,不然会无法生效。
<KeepAlive :include="['AVue','BVue','CVue']" >
<component :is="comId" ></component>
</KeepAlive>
缓存组件有两个生命周期钩子:
onActivated: 在组件挂载以及每次从缓存中插入时调用;
onDeactivated:在组件从dom移除,进入缓存或卸载时调用;
<script setup>
import { onActivated, onDeactivated } from 'vue';
onActivated(()=>{
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
console.log('我被加载了');
})
onDeactivated(()=>{
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
console.log('我被切换/卸载了')
})
</script>
推荐观看尚硅谷VUE3视频,天禹yyds,还以有小满zs的vue教程,很细。