Vue2到Vue3的改变
一.Vue2->Vue3
如果有Vue2的基础,并在此基础上学习Vue3,并不需要把完整的官网看完,我们只需要关注一下新功能和非兼容的变化即可进行开发.
二.Vue3变化
- 统一元素上使用的v-if和v-for优先级已更改,但不推荐同时使用v-if和v-for
- 组件事件需要在emits选项中声明
- destroyed生命周期选项被重命名为unmounted
- beforeDestroy生命周期选项被重命名为beforeUnmount
- 自定义指令的API已更改为组件生命周期一致
- 新增了三个组件:Fragment支持多个根节点 Suspense可以在组件渲染之前的等待时间显示指定内容 Teleport可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)
- 新增指令v-memo,可以缓存html模板,比如v-for列表不会变化的就缓存,简单说就是用内存换时间.
- 用Proxy代替Object.defineProperty重构了响应式系统,可以监听到数组下标变化,及对象新增属性,因为监听的不是对象属性,而是对象本身,还可拦截apply has等13种方法
- 重构了虚拟DOM, 在编译时会将事件缓存,将slot编译为lazy函数,保存静态节点直接复用(静态提升),以及添加静态标记,Diff算法使用最长递增子序列优化了对比流程,使得虚拟DOM生成速度提升200%
- 支持在<style></style>里使用v-bind,给CSS绑定JS变量(color:v-bind(str))
- 新增Composition API可以更好的逻辑复用和代码组织,同一功能的代码不至于像以前一样分散,虽然Vue2可以用mixins来实现复用代码,但也存在问题,比如方法或属性名会冲突,代码来源也不清楚等
- 全局函数set和delete以及实例方法$set和$delete移除.基于代理的变化检测以及不再需要它们了
- 毕竟Vue3是用TS写的,所以对TS的支持度更好
- Vue3不兼容IE11
- $on,$off和$once实例方法已被移除,组件实例不再实现事件触发接口
三.组合式API
原有的组件选项(data
、computed
、methods
、watch
) 的方式来组织组件代码通常是非常有效的,但是也存在一些不好的地方,例如把原有的关联逻辑按照选项划分开来,掩盖了原有潜在的逻辑问题,这个时候我们就必须要不断地上下滚动代码来找到响应的代码块来查找,这样带来了极大的不便,特别是一开始没有编写过这组件的人来说,这导致组件难以阅读和理解。
所以针对上述的情况,Vue3提出了新的组织组件代码的方式---组合式API。组合式API需要一个可以实际使用的地方,那就是setup
。
setup
的触发时机是在组件创建之前执行的。需要注意,在setup
中你应该避免使用this
,因为这个时候this
并不代表组件实例。setup
的调用发生在data
、computed
和methods
被解析之前,所以它们没法在setup
中被获取。
当然,我们依然可以在Vue3使用选项(Option API)的方式来组织代码,这个和Vue2没有区别,但是不建议这样写。
Vue3.x组件的选项(Option API)写法(不建议)
<script> import { defineComponent, ref } from 'vue'; export default defineComponent({ setup() {}, mounted() { console.log('生命周期mounted') }, components: {}, methods: {}, watch: {}, comuted: {} }); </script>
Vue3.x组合式写法(推荐)
<script> import { defineComponent, ref, onMounted } from 'vue'; export default defineComponent({ setup(){ let num = ref(0); let fn = () => {}; onMounted(():void => { console.log('生命周期mounted'); }); return { num, fn } } }) </script>
下面例子代码都是在Typescript的环境下进行的,所以需要Typescript基础
3.1.生命周期
通过在生命周期钩子前面加上"on"来访问组件的生命周期钩子.
下表包含如何在 Option API 和 setup() 内部调用生命周期钩子
Option API | setup |
---|---|
beforeCreate | - |
created | - |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
activated | onActivated |
deactivated | onDeactivated |
<template> <div id="div">123</div> </template> <script lang="ts"> import { defineComponent, onMounted } from "vue"; export default defineComponent({ setup() { console.log("setup"); onMounted((): void => { console.log("onMounted"); console.log(document.getElementById('div')?.innerHTML); }); }, }); </script>
3.2.ref,reactive,toRefs响应式和methods
Vue2.x默认写在data的值,初始化的时候内部会完成值的数据的响应式(get
、set
绑定),但是Vue3要手动调用内置方法实现,那么接下来看看常用的实现数据响应式方法都有哪些。
ref不仅可以用在数据的响应式,还可以绑定DOM
<template> <div> <div>{{ num }}</div> <button @click="add1">num++</button> <p>-------------------------------</p> <div>{{ state.count }}</div> <button @click="add2">state.count++</button> <p>-------------------------------</p> <div>{{ count }}</div> <button @click="add3">count++</button> </div> </template> <script lang="ts"> import { defineComponent, ref, reactive, toRefs } from "vue"; export default defineComponent({ setup() { interface ObjItf { count: number; } // ref声明响应式数据,用于声明基本数据类型 let num = ref<number>(1); let obj = { count: 1, }; // reactive声明响应式数据,用于声明引用数据类型 let state = reactive<ObjItf>(obj); // toRefs解构响应式数据 let { count } = toRefs<ObjItf>(state); const add1 = (): void => { num.value++; // 注意通过ref声明的变量,所以js要修改对应的值是要通过.value访问才可以,template模板不需要通过.value访问 }; const add2 = (): void => { state.count++; // 通过reactive声明的遍历,不需要通过.value访问值 }; const add3 = (): void => { count.value++; // 通过toRefs结构的值和ref声明的变量一样,需要通过.value访问其值 }; return { num, state, count, add1, add2, add3 }; }, }); </script>
注意:
- reactive可以传递基础数据类型和引用数据类型,基础数据类型不会被包装成响应式数据
- reactive返回的响应式数据本质是Proxy对象,对象里面每一层都会被包装成Proxy对象
- reactive返回的响应式数据和原始的数据会相互影响
- ref可以传递基础数据类型和引用数据类型,如果是基础数据类型,那么这个基础数据值保存在返回的响应式数据的.value上,如果是对象,响应式数据在.value上.
- ref本质是将一个数据变成一个对象,这个对象具有响应式特点
为什么需要toRefs和toRef?
和ref不一样的是,toRef和toRefs这两个方法,它们不创造响应式,而是延续响应式.创造响应式一般由ref和reactive来解决,而toRef和toRefs则把对象的数据进行分散和扩散,其这个对象针对的是响应式对象(reactive)而非普通对象.
3.3.watch
语法:watch(监听源 | [多个],(val,oldVal)=>{},{immediate?:false,deep:false})
watch写法上支持一个或者多个监听源,这些监听源必须只能是getter/effect函数,ref数据,reactive对象,或者是数组类型(只能是getter/effect函数,ref数据,reactive对象)
import { watch } from 'vue' //情况一:监视ref定义的响应式数据 watch(sum,(newValue,oldValue)=>{ console.log('sum变化了',newValue,oldValue) },{immediate:true}) //情况二:监视多个ref定义的响应式数据 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变化了',newValue,oldValue) }) /* 情况三:监视reactive定义的响应式数据 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!! 若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 */ watch(person,(newValue,oldValue)=>{ console.log('person变化了',newValue,oldValue) },{immediate:true,deep:false}) //此处的deep配置不再奏效 //情况四:监视reactive定义的响应式数据中的某个属性 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //情况五:监视reactive定义的响应式数据中的某些属性 watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{immediate:true,deep:true}) //特殊情况 watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
3.4.watchEffect
它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watch的套路是:既要指明监视的属性,也要指明监视的回调。
watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
两者都可以监听 data 属性变化。
watchEffect有点像computed:
但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
<script> let num = ref(0); watchEffect(() => { console.log(num.value); }); setTimeout(() => { num.value++; }, 1000); </script>
watch 和 watchEffect的区别
-
两者都可以监听
data
属性变化; -
watch
需要明确监听哪个属性; -
而
watchEffect
会根据其中的属性,自动监听其变化。
<template> <div>{{ count }}</div> <button @click="state.count++">state.count++</button> </template> <script lang="ts"> import { defineComponent, reactive, computed } from "vue"; export default defineComponent({ setup() { interface ObjItf { count: number; } let obj = { count: 1, arr: [1, 2, 3] }; const state = reactive<ObjItf>(obj); let count = computed((): number => { return state.count; }); return { count, state }; }, }); </script>
3.6.组件
全局组件:
const app = Vue.createApp({...}) app.component('my-component-name', { /* ... */ })
局部组件(子传父,父传子):
子组件mychild.vue:
<template> <div>{{ aname }}</div> </template> <script lang="ts"> import { defineComponent, toRefs } from "vue"; export default defineComponent({ props: { aname: { type: String, default: "张三", }, }, emits: ["uname"], setup(props, { emit }) { let { aname } = toRefs(props); const updateName = () => { emit('uname', 'a改变后的名字') }; return { aname, updateName, }; }, }); </script>
父组件:
<template> <mychild :aname="aname" @uname="updateName"></mychild> </template> <script lang="ts"> import { defineComponent, toRefs, ref } from "vue"; import mychild from './mychild.vue'; export default defineComponent({ components: { mychild }, setup() { let aname = ref('李四'); const updateName = (p: string) => { aname.value = p; } return { aname, updateName } } }) </script>
<!-- --> <template> <div>{{ aname }}</div> <button @click="updateName">修改姓名</button> </template> <script lang="ts" setup> // setup语法糖下defineProps,defineEmits 不需要引入 let emit = defineEmits(["updatename"]); let props = defineProps({ aname: { type: String, default: "李四", }, }); const updateName = (): void => { emit('updatename', '修改后的名字') }; </script> <style lang="less" scoped></style>
父组件:
<!-- --> <template> <div></div> <mychild :aname="aname" @updatename="updateName"></mychild> </template> <script lang="ts" setup> import { ref } from "vue"; import mychild from "./b.vue"; let aname = ref("张三"); let updateName = (name: string): void => { aname.value = name; }; </script> <style lang="less" scoped></style>
3.7.v-model
子组件:
<!-- --> <template> <div>{{ name }}{{ age }}</div> <button @click="updateName">修改姓名</button> </template> <script lang="ts" setup> let props = defineProps({ age: Number, name: String, }); let emits = defineEmits(["update:name", "update:age"]); let updateName = () => { emits("update:age", 30); emits("update:name", "李四"); }; </script> <style lang="less" scoped></style>
父组件:
<!-- --> <template> <div></div> <mychild v-model:name="state.name" v-model:age="state.age"></mychild> </template> <script lang="ts" setup> import { reactive } from "vue"; import mychild from "./b.vue"; let info = { name: '张三', age: 20 } let state = reactive(info); </script> <style lang="less" scoped></style>
3.8.插槽
子组件:
<!-- --> <template> <slot></slot> <slot name="title"></slot> <slot name="footer" :user="state.user" :d="state.d"></slot> </template> <script lang="ts" setup> import { reactive } from "vue"; let state = reactive({ user: { a: 1, b: 2 }, d: 1, }); </script> <style lang="less" scoped></style>
父组件:
<!-- --> <template> <div></div> <mychild> <div>匿名插槽</div> <template #title> <div>具名插槽</div> </template> <template #footer="scope"> <div>作用域插槽{{ scope.user }} {{ scope.d }}</div> </template> </mychild> </template> <script lang="ts" setup> import mychild from "./child.vue"; </script> <style lang="less" scoped></style>
3.9.await支持
不必再配合async就可以直接使用await了,这种情况下,组件的setup会自动变成async setup.
<script setup> const post = await fetch('/api').then(() => {}) </script>
如果浏览器报了这个错误的话
在app组件的模板最外层嵌套<Suspense>标签
App.vue:
3.10.style scoped
<style scoped> /* 修改第三方组件样式 */ ::v-deep(.foo) {} /* 简写 */ :deep(.foo) {} /* 修改插槽内容样式 */ ::v-slotted(.foo) {} /* 简写 */ :slotted(.foo) {} /* 修改全局样式 */ ::v-global(.foo) {} /* 简写 */ :global(.foo) {} </style>
3.11.teleport
这个组件可以把组件进行传送,to属性就是要传送的位置目标,用css选择器
<template> <div class="header"></div> <teleport :to="target"> <p>哈哈哈哈哈</p> </teleport> <button @click="changeTarget">点击切换</button> <p>-------------------------------</p> <div class="footer"></div> </template> <script lang="ts" setup> import { ref } from "vue"; let target = ref<string>(".header"); const changeTarget = (): void => { target.value = ".footer"; }; </script> <style lang="less" scoped></style>