前言
Vue3已经发布一段时间了,前不久在项目中使用了Vue3,借这篇文章来讲述一下Vue3的实践方法,以及分享一下开发过程中遇到的一些问题,希望能对大家有一些帮助。
一、为什么重写Vue2.X
尤雨溪的回答是两个关键因素:
- 主流浏览器对新的
JavaScript语言特性的普遍支持 - 当前
Vue代码库随着时间的推移而暴露出来的设计和体系架构问题
二、安装Vue3
- 通过脚手架
Vite
npm init vite hello-vue3 | yarn create vite hello-vue3
- 通过脚手架
vue-cli
npm install -g @vue/cli | yarn global add @vue/cli
vue create hello-vue3
# 选择 vue 3 preset
三、组件基本结构分析
下面以helloWorld.vue组件为例讲述
//dom 里的东西基本与Vue2相同
<template>
<h1>{{ msg }}</h1>
<button @click="increment">
count: {{ state.count }}, double: {{ state.double }},three:{{ three }},refnum:{{refnum}}
</button>
</template>
<script>
// 这里就是Vue3的组合Api了,需要的api要手动import引入
import {ref, reactive, computed ,watchEffect,watch} from "vue"
export default {
name: "HelloWorld",
props: {
msg: {
type: String,
default: ''
}
},
// 这里的setup相当于Vue2的beforeCreate 和created,简单理解就是初始化
setup() {
// 这里通过reactive使state成为响应状态
const state = reactive({
count: 0,
// 计算属性computed可以直接在state中使用,更灵活了
double: computed(() => state.count * 2),
})
//computed也可以单独拿出来使用
const three = computed(() => state.count * 3)
//ref跟reactive作用一样都是用来数据响应的,ref的颗粒度更小
const refnum = ref()
//这里的watchEffect只要里面的变量发生了改变就会执行,并且第一次渲染会立即执行,无法获取到变化前的值
watchEffect(() => {
refnum.value = state.count
console.log(state, "watchEffect")
})
//watch的使用方法与Vue2相同,第一个参数是监听需要的变量,第二个是执行的回调函数,
watch(refnum,(a,b)=>{
console.log(a,b,'watch,a,b')
})
//所有的方法里再也不需要用this了
function increment() {
state.count++
}
//组中模板中需要的变量,都要通过return给暴露出去
return {
state,
increment,
three,
refnum
};
},
};
</script>
四、Vue3新特性
1. 组合式API
setup是Vue3新增的一个组件选项,在组件被创建之前,props被解析之后执行,是组合式API的入口
setup参数
- props 组件传入的属性
props是响应式的,会及时被更新,由于是响应式的,所以不可以使用ES6解构,解构会消除他的响应式
- context
由于setup中不能访问Vue2中最常用的this对象,所以context提供了this中最常用的三个属性:attrs、slot和emit,分别对应Vue2中的$attr属性、slot插槽和$emit发射事件
script setup 语法糖
setup script是Vue3新出的一个语法糖,使用方法就是在书写script标签的时候在后面加一个setup修饰,这与普通的script只在组件被首次引入的时候执行一次不同,<script setup>中代码会在每次组件实例被创建的时候执行。
例如:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
在<script setup>中需要使用defineProps和defineEmits来声明props和emits,他们会提升到模块范围,所以引用时直接使用变量名即可:
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
</script>
<script setup>默认是关闭的,通过ref或者$parent链不能获取组件内的属性,想要获取组件内的属性需要使用defineExpose来暴露出可以访问的属性:
<script setup>
import { ref, defineExpose } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
在<script setup>中需要使用useSlots和useAttrs来调用slots和attrs:
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
<script setup>可以和普通的<script>一起使用,例如inheritAttrs或通过插件启用的自定义选项、声明命名导出、运行副作用或者创建只需要执行一次的对象等情况,必须用到<script>,但setup只有一个可以生效(后面的会覆盖前面的)
<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
使用语法糖优点:
- 自动注册组件;
- 属性和方法无需返回
响应式变量的定义 ref、toRef、reactive、toRefs
看到这些,大家可能很疑惑,无从下手,下面我们就先来看一下它们的使用方式
<template>
<div>哈哈哈</div>
<HelloWorld ref='helloWorldRef'></HelloWorld>
</template>
<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'
import HelloWorld from '@/components/HelloWorld.vue'
// 为子组件或html元素绑定ref,函数中使用通过helloWorldRef.value访问
const helloWorldRef = ref()
// 初始化响应式变量tor,函数中取值通过tor.value
let tor = ref(0)
const obj = { age: 12 }
// 将对象的某个属性转化为响应式,并设置key值,函数中取值通过toR.value
let toR = toRef(obj, 'age')
// 初始化响应式变量,传入的值需为引用类型的值,例如数组、对象等
// reactive内部可以使用计算属性等各种方法,跟ref混合使用时可以用isRef判断类型
// 在模板中可以使用state.num访问,通过toRefs转换后在模板中可以直接使用num访问
const state = reactive({
num: 1,
name: '张三'
})
return {
helloWorldRef,
tor,
toR,
// toRefs可以将使用了reactive的响应式对象拆分成多个响应式ref,外界可以读取到响应式的所有属性
...toRefs(state)
}
</script>
来总结一下:
ref可以将一个基本数据类型的属性转换为响应式,使用在对象的某个属性时,是对该属性的拷贝,但拷贝后的值改变不会原对象属性的值toRef可以将对象的某个基本数据类型的属性转换为响应式,是对该属性的引用,所以值改变后会影响原对象的值toRefs是原对象数据的引用,值改变后会影响原对象的值,但必须要和reactive连用reactive可以将引用类型的数据转换为响应式,访问不用加.value
watch、watchEffect
看名字就知道两者都是用来监听的,但是又存在着一些区别,下面我们先来看一下watch的使用方式
watch(source, callback, [options])
// watch的使用方式和Vue2相同,可接收三个参数
// source:需要监听的数据源,支持基本数据类型、object、function、array
// callback:执行的回调函数
// options:支持deep、immediate和flush选项
watch监听不同的数据源,使用方式略有不同
<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'
let tor = ref(0)
const obj = { age: 12 }
let toR = toRef(obj, 'toR')
const state = reactive({
num: 1,
name: '张三',
room: {
id: 1,
attrs: {
size: 100,
type: '两室一厅'
}
}
})
// 监听reactive定义的数据
watch(
() => state.name,
(newValue, oldValue) => {
console.log("新值:", newValue, "老值:", oldValue)
}
)
// 监听ref定义的数据
watch(
tor,
(newValue, oldValue) => {
console.log("新值:", newValue, "老值:", oldValue)
}
)
// 监听多个数据
watch(
[() => state.name, tor],
([newName, newTor], [oldName, oldTor]) => {
console.log("新值:", newName, "老值:", oldName)
console.log("新值:", newTor, "老值:", oldTor)
}
)
// 监听深度嵌套的对象
watch(
() => state.room,
(newValue, oldValue) => {
console.log("新值:", newValue, "老值:", oldValue)
},
{ deep: true }
)
// stop停止监听,组件中创建的watch监听会在销毁时自动停止,但如果我们想在组件销毁之前停止监听,可以通过调用`watch()`函数的返回值
const stopWatch = watch(
() => state.name,
(newValue, oldValue) => {
console.log("新值:", newValue, "老值:", oldValue)
}
)
setTimeout(() => {
stopWatch()
}, 3000)
</script>
上面介绍了watch的使用方法,基本上已经满足我们需要的所有监听需求了,下面再来看一下watchEffect的使用方式吧
<script setup>
import { ref, toRef, reactive, toRefs } from 'vue'
let tor = ref(0)
const obj = { age: 12 }
let toR = toRef(obj, 'toR')
const state = reactive({
num: 1,
name: '张三'
})
// watchEffect使用方式
watchEffect(() => {
console.log(state, tor)
})
</script>
最后总结一下二者的区别
watch需要传入需要监听的属性;watchEffect不需要传入参数watch是惰性的,只有在监听属性变化时才会执行,可通过传入immediate参数使其在初始化时执行;watchEffect第一次会立即执行wacth的回调方法会返回变化前后的值;watchEffect只可以拿到变化后的值watch可以reactive绑定的对象,watchEffect不可以(reactive的值要具体到内部属性),只会执行一次
生命周期
我们从生命周期图来看一下生命周期的变化
最后来总结一下各声明周期钩子在
setup中的名称
| 选项式 API | 调用时机 | setup |
|---|---|---|
| beforeCreate | 在实例初始化之后、进行数据侦听和事件/侦听器的配置之前同步调用 | 不需要 |
| created | 在实例创建完成后被立即同步调用 | 不需要 |
| beforeMount | 在挂载开始之前被调用 | onBeforeMount |
| mounted | 在实例挂载完成后被调用 | onMounted |
| beforeUpdate | 在数据发生改变后,DOM 被更新之前被调用 | onBeforeUpdate |
| updated | 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用 | onUpdated |
| beforeUnmount | 在卸载组件实例之前调用 | onBeforeUnmount |
| unmounted | 卸载组件实例后调用 | onUnmounted |
| errorCaptured | 在捕获一个来自后代组件的错误时被调用 | onErrorCaptured |
| renderTracked | 跟踪虚拟 DOM 重新渲染时调用 | onRenderTracked |
| renderTriggered | 当虚拟 DOM 重新渲染被触发时调用 | onRenderTriggered |
| activated | 被 keep-alive 缓存的组件激活时调用 | onActivated |
| deactivated | 被 keep-alive 缓存的组件失活时调用 | onDeactivated |
整体来看其实变化不大,使用setup代替了之前的beforeCreate和created,其他生命周期名字有些变化,功能都是没有变化的;另外除了beforeCreate和created,其他的生命周期钩子还是可以在setup外调用的,setup中调用的生命周期钩子函数可以重复调用,有助于代码的集中管理
2. Teleport
teleport是Vue3新推出的功能,这个词翻译过来是传送的意思,意为将模板传动到DOM中Vue app之外的其他位置。
下面来看一下teleport的使用方式:
<!-- index.html -->
<body>
<div id="app"></div>
<!-- modal需要渲染的位置 -->
<div id="modal"></div>
</body>
然后我们再来看一下modal组件的具体定义
<template>
<!-- 传送到index.html文件中id为modal的dom下 -->
<teleport to="#modal">
<div class="modal" v-if="show">
<!-- 模态框内容 -->
<slot></slot>
</div>
</teleport>
</template>
最后我们来看一下在helloWorld组件中引用model组件
<template>
<div class="hello-world">
<p class="text">Hello World!</p>
<el-button type="primary" size="small" @click="handleShowModal">展示Modal</el-button>
<modal :show="show" @handleCloseModal="handleCloseModal">
<div class="modal-content">
<p class="icon-close" @click="handleCloseModal">X</p>
<p class="modal-content-text">这里是modal</p>
</div>
</modal>
</div>
</template>
<script>
import { ref } from 'vue'
import modal from './components/modal.vue'
export default {
name: 'helloWorld',
components: {
[modal.name]: modal
},
setup() {
let show = ref(false)
const handleShowModal = () => {
show.value = true
}
const handleCloseModal = () => {
show.value = false
}
return {
show,
handleShowModal,
handleCloseModal
}
}
}
</script>
我们来看一下效果
可以看到我们虽然是在
helloWorld组件中引用的modal组件,但是modal却渲染在了和app同级的modal中,但是modal的显示却是由helloWorld组件控制的。
最后来总结一下:teleport可以将包裹的内容传送到index.html文件中除<div id="app"></div>之外的其他位置
使用场景:像 modal,toast 等这样的元素,需要使用到 Vue 组件的状态(data 或者 props)的值,但是又想在在 Vue 应用的范围之外渲染它
原因在于如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难
3. 片段
在Vue2中,组件内只被允许有一个根节点,所以许多组件都被包裹在一个<div>中,像这样:
<template>
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</template>
现在Vue3中,组件允许你定义多个根节点,这也是Vue3的一个新特性,像这样:
<template>
<header></header>
<main></main>
<footer></footer>
</template>
4. 触发组件选项
这一部分主要从父子组件发射事件与绑定属性说明
- 事件名
与Vue2用法类似,可自动转换大小写,即子组件中触发一个以驼峰式命名的事件,可以在父组件中添加一个短横线分割命名的监听器。
// 子组件
this.$emit('myEvent')
// 父组件
<my-component @my-event="doSomething"></my-component>
- 1)定义自定义事件
在Vue3中子组件发射的事件需要集中在
emits中定义,类似于props的用法
emits: ['inFocus', 'submit']
事件与props的类型验证类似,也可以验证抛出的事件,使用对象语法而不是数组语法定义发出的事件,就可以对它进行验证
emits: {
// 没有验证
click: null,
// 验证 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
context.emit('submit', { email, password })
}
}
- 2)
v-model参数
v-model在Vue2中也可以传参给子组件,并且子组件可以修改父组件的值,但是一个组件只可定义一个v-model且子组件接收只能通过value;若子组件想修改多个父组件的值,需要使用.sync属性,但Vue3中删除了.sync属性,Vue3中子组件想修改父组件的变量值,可以通过v-model来实现
默认情况下,父组件上的v-model使用modelValue作为prop,子组件中的update:modelValue作为事件
// 父组件
<my-component v-model="bookTitle"></my-component>
// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'
export default {
props: {
modelValue: {
type: String,
defaule: ''
}
},
emits: ['update:modelValue'],
setup(props, context) {
context.emit('update:modelValue', 'newValue')
}
}
</script>
我们也可以通过向v-model传递参数来修改这些名称
// 父组件
<my-component v-model:title="bookTitle"></my-component>
// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'
export default {
props: {
title: {
type: String,
defaule: ''
}
},
emits: ['update:title'],
setup(props, context) {
context.emit('update:title', 'newValue')
}
}
</script>
我们也可以绑定多个v-modal,每个v-model将同步到不同的prop
// 父组件
<my-component v-model:title="bookTitle" v-model:content="bookContent"></my-component>
// 子组件
<script>
import { ref, toRef, reactive, toRefs } from 'vue'
export default {
props: {
title: {
type: String,
defaule: ''
},
content: {
type: String,
defaule: ''
}
},
emits: ['update:title', 'update:content'],
setup(props, context) {
context.emit('update:title', 'newTitle')
context.emit('update:content', 'newContent')
}
}
</script>
5. 单文件组件状态驱动的css变量
单文件组件的<style>标签可以通过v-bind这一CSS函数将CSS的值关联到动态的组件状态上:
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red'
}
}
}
</script>
<style>
.text {
color: v-bind(color);
}
</style>
在<script setup>中需要这样使用:
<script setup>
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
6. Suspense
Suspense是Vue3中的新增特性,当前处于试验阶段,生产环境不建议使用,下面我们就简单来看一下它的作用
背景:前端页面的展示内容通常依赖于异步接口返回的结果,在此之前为了交互友好我们通常会添加一个loading或者骨架屏,等拿到数据中再进行展示,在Vue2中我们是通过v-if判断来控制的。
而Suspense就是为了解决这一问题的,它提供了两个插槽,他们都只接收一个子节点,当default中的节点不能展示时,会展示fallback插槽里的节点,只到default中的节点可以正常展示为止
<template>
<suspense>
<template #default>
<todo-list />
</template>
<template #fallback>
<div>
Loading...
</div>
</template>
</suspense>
</template>
<script>
export default {
components: {
TodoList: defineAsyncComponent(() => import('./TodoList.vue'))
}
}
</script>
注意:使用Suspense,需要返回一个promise
五、变更
这里主要说一下插槽、自定义指令、异步组件的变更,其他变更建议查看官方文档
1. slot
在Vue2中我们这样使用具名插槽:
<!-- 子组件中:-->
<slot name="title"></slot>
在父组件中调用:
<template slot="title">
<p>title</p>
<template>
在Vue2中我们这样使用作用域插槽:
// 子组件
<slot name="content" :data="content"></slot>
export default {
data(){
return {
content: 'content'
}
}
}
在父组件中调用:
<template slot="content" slot-scope="scoped">
<p>{{ scoped.data }}</p>
<template>
在Vue3中将slot和slot-scope进行了合并,统一通过v-slot来使用:
<!-- 父组件中使用 -->
<template v-slot:content="scoped">
<p>{{ scoped.data }}</p>
<template>
<!-- 也可以简写成: -->
<template #content="{data}">
<p>{{ data }}</p>
</template>
2. 自定义指令
还记得在Vue2中我们如何实现一个自定义指令吗
// 注册一个全局的v-highlight指令
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
Vue3将指令的钩子函数重新命名,现与组件的生命周期保持一致,expression字符串不再作为binding对象的一部分被传入
| Vue2 | Vue3 | 备注 |
|---|---|---|
| created | 新增,在元素的attributte或事件监听器被应用之前调用 | |
| bind | beforeMount | 指令绑定到元素后调用,只调用一次 |
| inserted | mounted | 元素插入父DOM后调用 |
| beforeUpdate | 新增,在元素本身被更新之前调用,与组件的生命周期钩子十分相似 | |
| update | 移除 | |
| componentUpdated | updated | 一旦组件和子级被更新,就会调用这个钩子 |
| beforeUnmount | 新增,与组件的生命周期钩子类似,它将在元素被卸载之前调用 | |
| unbing | unmounted | 一旦指令被移除,就会调用这个钩子,只调用一次 |
在Vue3我们这样来自定义指令
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
3. 异步组件
Vue3中使用defineAsyncComponent来定义异步组件,配置选项component替换为loader,loader函数不在接收resolve和reject参数,且必须返回一个Promise
<template>
<!-- 异步组件的使用 -->
<AsyncPage />
</tempate>
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// 无配置项异步组件
AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
// 有配置项异步组件
AsyncPageWithOptions: defineAsyncComponent({
loader: () => import(".NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: () => import("./ErrorComponent.vue"),
loadingComponent: () => import("./LoadingComponent.vue"),
})
},
}
</script>
注意:
defineAsyncComponent与suspense一起用时,defineAsyncComponent配置的延迟、超时、错误、加载选项都将被忽略- 实践中发现子组件返回
promise并使用defineAsyncComponent同时使用时,子组件不显示,必须与suspense同时使用才可以
4. 过滤器
vue3中去掉了filter,官方建议使用computed或者method来代替
- 使用
computed
computed: {
computedText() {
// 计算属性要return一个函数接收参数
return function (state) {
switch (state) {
case "1":
return "待发货";
break;
case "2":
return "已发货";
break;
case "3":
return "运输中";
break;
case "4":
return "派件中";
break;
case "5":
return "已收货";
break;
default:
return "快递信息丢失";
break;
}
};
},
},
- 使用
method
methods: {
methodsText(state) {
switch (state) {
case "1":
return "待发货";
break;
case "2":
return "已发货";
break;
case "3":
return "运输中";
break;
case "4":
return "派件中";
break;
case "5":
return "已收货";
break;
default:
return "快递信息丢失";
break;
}
},
},