Vue3基础看这一篇就够了(万字长篇,附实例代码及效果演示)
目录
前言
概述
Vue3组合式api VS Vue2选项式api
基础部分
setup
选项式api的风格
组合式api的风格
区别
响应式数据
ref
reactive
shallowReactive 与 shallowRef
计算属性和监听
computed 函数
watch 函数
watchEffect
生命周期
响应式数据只读
toRaw 返回代理的源
markRaw 标记对象拒绝代理
provide 与 inject 跨组件传值
判断是否为响应式数据
toRef 和 toRefs 解构响应式数据
新组件
Fragment
Teleport
Suspense
组合式函数
全局的api及指令的变动
结语
前言
vue3已经出了好长一段时间了,最近闲来无事简单学习了一下,新增的东西还是挺多的,写一篇文章来记录一下。
概述
Vue3组合式api VS Vue2选项式api
谈到 vue3,首先想到的就是组合式api,很大程度的解决了vue2选项式api的缺点,那有啥缺点?当文件中的业务代码非常多的时候,阅读修改代码的时候是非常痛苦的,data,method,watch还有计算属性之间来回跳转, 我已经准备拔刀了。
下面这些图被疯转,很形象的展现了vue2和vue3的区别,可以看到组合式api就是将单个功能的状态,方法,计算属性等等需要用到的东西都组合在一起抽离成一个hook,也就是对应图4的function,最终再统一引入组合到一起。这样做的好处就是单个功能的代码都在一起,方便调式修改。
基础部分
setup
setup是vue3的一个新的配置项,只在初始化的时候执行一次,所有的组合式函数都在此使用。setup可以在选项式api的风格中使用也可以通过组合式api的风格 。通过代码简单对比一下。vue3推荐使用组合式。
选项式api的风格
import { ref } from 'vue' export default { setup() { const sum = ref(1) return { sum, } }, }v3
{{ sum }}
+1
组合式api的风格
import { ref } from 'vue' const sum = ref(1)v3
{{ sum }}
+1
区别
- 中的导入和顶层变量/函数都能够在模板中直接使用, 选项式则需要导出
- 打包出来的体积更小
- 对ts更友好
官网介绍的比较详细,感兴趣可以查看组合式 API 常见问答 | Vue.js
响应式数据
vue2中 data 函数返回的对象就是响应式的数据,但是在增加删除对象属性时不是响应式的,当然vue2中也有对应的解决方法,this.$set(), this.$delete(), 其实这也能够理解,毕竟vue2的响应式式基于 Object.defineProperty 实现的,这个函数只提供了 get 和 set 以及一些描述符 descriptor,并没有提供 add 和 delete 方法。
vue3中的响应式包含了两种形态, ref(底层还是Object.defineProperty进行数据劫持, 处理简单数据类型),reactive(使用es6的Proxy进行数据劫持,处理复杂数据类型),完全修复了vue2响应式的痛点,vue3的响应式更加的友好。
ref
ref 接受一个值,返回一个响应式对象,一般用来处理简单数据类型的响应式,但如果传入的值是对象 ref 会求助 reactive,返回RefImpl的实例简称ref对象。 此时可能会有疑惑,既然ref是一个响应式的对象,为什么模板中能正常解析。这是因为在解析templete时遇到ref对象会自动取其value属性,但是如果要在方法中修改ref创建的响应式数据,你的写法应该是这样的 state.value = xxx
import { ref } from 'vue' const sum = ref(1) function add() { sum.value++ }v3
{{ sum }}
+1
reactive
为对象做深层!!!!响应式代理, 也就是如果对象有多层依旧是响应式的,返回一个Proxy实例, 如果传入一个字符串或者数字,它将不是响应式的。Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)Proxy - JavaScript | MDN。Vue使用 Proxy 进行数据劫持, Reflect 进行反射修改 Reflect - JavaScript | MDN
import { reactive } from 'vue' const person = reactive({ name: '张三', age: 12, job: { j1: { jname: '前端开发', }, }, }) function add() { person.hobby = ['唱', '跳', 'rap'] } function deleteHB() { delete person.hobby }v3
{{ sum }}
姓名:{{ person.name }}
年龄:{{ person.age }}
工作:{{ person.job.j1.jname }}
爱好: {{ person.hobby }}
修改姓名 修改年龄 修改工作 增加爱好 删除爱好
shallowReactive 与 shallowRef
shallowRef 直译过来意思是浅层的 ref,shallowRef 传入对象不会求助 reactive,仅仅对ref对象的 value 属性具有响应式。
shallowReactive 只处理对象第一层的响应式, 如果修改了深层的数据页面是不会响应的,但是会在下次页面更新中渲染出来。
import { shallowReactive, shallowRef, ref, reactive } from 'vue' const shallowRef_jack = shallowRef({ name: 'jack', sex: '女' }) const shallowReactive_ben = shallowReactive({ name: 'ben', sex: '女', child: { son: { name: '张三', }, }, }) const ref_jack = ref({ name: 'jack', sex: '女' }) const reactive_ben = reactive({ name: 'ben', sex: '女', child: { son: { name: '张三', }, }, })v3
shallowRef_jack: {{ shallowRef_jack }} 修改整个对象 修改对象属性
ref_jack: {{ ref_jack }} 修改整个对象 修改对象属性
shallowReactive_ben: {{ shallowReactive_ben }} 修改对象的第三层属性 修改对象第一层属性
reactive_ben: {{ reactive_ben }} 修改对象的第三层属性 修改对象第一层属性
h3 { font-size: 26px; border: 1px solid #ccc; padding: 20px; margin: 20px; } button { float: right; padding: 10px; font-size: 20px; }
计算属性和监听
computed 函数
计算属性有两种写法,作用和vue2一样,通过监听某个值的变化计算出一个新值
- 只读的写法 :computed(() => xxxxxx),
- 可读可写的写法: computed({ get: () => xxxx, set: (val) => { xxxx } })
import { ref, computed } from 'vue' const count = ref(1) const num1 = computed(() => count.value + 1) const num2 = computed({ get() { return count.value + 1 }, set(val) { count.value = val + 1 }, })
v3
ref 定义的 count: {{ count }} count++
计算属性 num1: {{ num1 }} num1++
计算属性 num2: {{ num2 }} num2++
watch 函数
watch 函数用来监听数据的变化,和vue2大体上都是相同的。
参数列表:
- 参数1为需要监听的响应式对象(可以是单个对象,也可以是一个数组,也可以是一个getter函数),
- 参数2为监听对象发生变化时所执行的回调
- 参数3是一些配置项:immediate是否开启立即监听,deep是否开启深度监听,flush回调的触发时机,onTrack / onTrigger用于调试的两个函数
注意点:
- 直接监听 reactive 定义的响应式对象默认开启了深度监听
- 通过 getter 形式监听响应式对象默认是浅层监听
import { reactive, ref, watch } from 'vue' const count = ref(1) const person = reactive({ name: 'ben', child: { son: { name: 'zs', }, }, }) // 监听 ref 对象 watch(count, (val, preVal) => { console.log('count变化了', val, preVal) }) // 监听 reactive 定义的响应式对象 watch(person, (val, preVal) => { console.log('person变化了', val, preVal) }) watch([count, person], (val, preVal) => { console.log('person变化了或count变化了', val, preVal) })
v3
ref 定义的 count: {{ count }} count++
reactive 定义的 person: {{ person }} 修改姓名 修改儿子姓名
watchEffect
watchEffect 函数用于监听传入的函数内访问的所有响应式数据的变化。白话一点就是回调里我用了谁我就监听谁,监听ref定义的响应式数据时,不要忘记 .value ,哥们就是这么智能。
watch 和 watchEffect 都是监听数据变化的函数,和 react 中的 useState 放入依赖项有着异曲同工之妙。
例子:切换下拉框中的 name ,模拟请求后台接口
import { onMounted, reactive, ref, watchEffect } from 'vue' const name = ref('jack') const info = [ { id: 1, name: 'jack', child: { son: { name: 'zs', }, }, }, { id: 2, name: 'ben', child: { son: { name: 'zs', }, }, }, ] let data = ref([]) async function getInfoByName(name) { const res = await new Promise((reslove) => { setTimeout(() => { reslove(info.filter((item) => item.name === name)) }, 500) }) data.value = res } watchEffect(async () => { getInfoByName(name.value) })
v3
{{ item.name }}的个人信息 {{ item }}生命周期
vue3的生命周期稍有变动,增加了 setup 钩子,且销毁前和销毁后的钩子命名更改为 beforeUnmount 和 unmounted,以下代码是验证的一些示例
App.vue
import Demo from './Demo.vue' import Demo2 from './Demo2.vue' import { ref } from 'vue' const isComDestory = ref(true) const isOptionDestory = ref(true)
v3 引入组合式子组件 销毁组合式子组件 引入选项式子组件 销毁选项式子组件
button { padding: 20px; font-size: 16px; }Demo.vue
import { onMounted, onBeforeMount, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, ref, } from 'vue' const sum = ref(1) console.log('子组件1 setup') onBeforeMount(() => { console.log('子组件1 onBeforeMount') }) onMounted(() => { console.log('子组件1 onMounted') }) onBeforeUpdate(() => { console.log('子组件1 onBeforeUpdate') }) onUpdated(() => { console.log('子组件1 onUpdated') }) onBeforeUnmount(() => { console.log('子组件1 onBeforeUnmount') }) onUnmounted(() => { console.log('子组件1 onUnmounted') })
我是子组件1
{{ sum }} +1
div { border: 1px solid #ccc; }Demo2.vue
import { ref } from 'vue' export default { setup() { const sum = ref(1) console.log('子组件2 setup') return { sum } }, beforeCreate() { console.log('子组件2 beforeCreate') }, created() { console.log('子组件2 created') }, beforeMount() { console.log('子组件2 beforeMount') }, mounted() { console.log('子组件2 mounted') }, beforeUpdate() { console.log('子组件2 beforeUpdate') }, updated() { console.log('子组件2 updated') }, beforeUnmount() { console.log('子组件2 beforeUnmount') }, unmounted() { console.log('子组件2 unmounted') }, }
我是子组件2
{{ sum }} +1
div { border: 1px solid #ccc; }由于录频录不了控制台,打印结果看下图
响应式数据只读
vue3提供了两个api,限制响应式数据为只读,不可修改。分别为 readonly(深层只读) 和shallowReadonly (浅层只读)
import { ref, reactive, readonly, shallowReadonly } from 'vue' const sum = readonly(ref(1)) const p1 = readonly( reactive({ name: 'ben', child: { son: { name: 'jack', }, }, }) ) const p2 = shallowReadonly( reactive({ name: 'ben', child: { son: { name: 'jack', }, }, }) ) function edit() { sum.value = 2 p1.name += '!' p1.child.son.name += '&' } function editShallow() { p2.name += '!' p2.child.son.name += '&' }
v3
readonly: {{ sum }}
readonly: {{ p1 }}
shallowReadonly: {{ p2 }}
修改深层只读数据 修改浅层只读数据toRaw 返回代理的源
toRaw的功能官网的解释很清晰, 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象
import { ref, reactive, readonly, shallowReadonly, shallowReactive, toRaw, } from 'vue' const p1 = readonly( reactive({ name: 'a', child: { son: { name: 'as', }, }, }) ) const p2 = shallowReadonly( reactive({ name: 'b', child: { son: { name: 'bs', }, }, }) ) const p3 = reactive({ name: 'c', child: { son: { name: 'cs', }, }, }) const p4 = shallowReactive({ name: 'd', child: { son: { name: 'ds', }, }, }) console.log('toRaw p1 readonly', toRaw(p1)) console.log('toRaw p2 shallowReadonly', toRaw(p2)) console.log('toRaw p3 reactive', toRaw(p3)) console.log('toRaw p4 shallowReactive', toRaw(p4))
markRaw 标记对象拒绝代理
markRaw()将对象标记为不可代理,返回其本身。本身上多了一个 __v_skip 属性表示忽略代理。强行代理代理是无效的,返回的还是其本身而不是响应式对象。
import { markRaw, reactive } from 'vue' const p1 = { name: 'a', child: { son: { name: 'as', }, }, } const noProxy_p1 = markRaw(p1) console.log('不可代理对象', noProxy_p1) console.log('reactive 代理不可代理对象', reactive(noProxy_p1))
provide 与 inject 跨组件传值
使用 provide 与 inject 进行跨组件传值十分方便。以父子孙为例,父组件 provide ('name',value) 子组件 inject ('name') 即可
父组件
import { reactive, provide } from 'vue' import Demo from './Demo.vue' const obj = { name: 'a', child: { son: { name: 'as', }, }, } const person = reactive(obj) provide('person', person)
父组件
{{ person }}
.father { padding: 10px; background: orange; }子组件
import Demo2 from './Demo2.vue'
子组件
div { padding: 10px; background: salmon; border: 1px solid #ccc; }孙组件
import { ref, inject } from 'vue' export default { setup() { const person = inject('person') return { person } }, }
孙组件
{{ person }}
.sonson { background: sandybrown; border: 1px solid #ccc; }判断是否为响应式数据
- isRef(data)判断data是否是通过ref创建的响应式数据
- isReactive(data)判断data是否是通过reactive创建的响应式数据
- isReadonly(data)判断data是否是通过readOnly创建的只读数据
- isProxy(data)判断data是否为Proxy代理对象
import { reactive, readonly, ref, isProxy, isReactive, isRef, isReadonly, } from 'vue' const person = reactive({ name: 'a', child: { son: { name: 'as', }, }, }) const num = ref(1) const str = readonly(ref('str')) console.log(isRef(num)) console.log(isReactive(person)) console.log(isReadonly(str)) console.log(isProxy(person), isProxy(str))
toRef 和 toRefs 解构响应式数据
当响应式对象的属性过多且页面用到很多次的时候, toRef 和 toRefs 可以进行响应式解构,解构出来的数据依旧具备响应式的能力。下面的例子是在 中进行演示的,setup()中的需要显示的返回
toRef
import { reactive, toRef } from 'vue' const person = reactive({ name: 'a', age: 18, child: { son: { name: 'as', }, }, }) const personName = toRef(person, 'name') const personAge = toRef(person, 'age') const personSonName = toRef(person.child.son, 'name')
toRef 解构出 person的name ----- {{ personName }}
toRef 解构出 person的age ----- {{ personAge }}
toRef 解构出 person的child的son的name ----- {{ personSonName }}
toRef 解构出 person的name ----- {{ personName }}
toRef 解构出 person的age ----- {{ personAge }}
toRef 解构出 person的child的son的name ----- {{ personSonName }}
toRef 解构出 person的name ----- {{ personName }}
toRef 解构出 person的age ----- {{ personAge }}
toRef 解构出 person的child的son的name ----- {{ personSonName }}
修改person的name 修改person的age person的child的son的nametoRefs
import { reactive, toRefs } from 'vue' const person = reactive({ name: 'a', age: 18, child: { son: { name: 'as', }, }, }) const { name, age, child } = toRefs(person)
toRefs 解构出 person的name ----- {{ name }}
toRefs 解构出 person的age ----- {{ age }}
toRefs 解构出 person的child的son的name ----- {{ child.son.name }}
toRefs 解构出 person的name ----- {{ name }}
toRefs 解构出 person的age ----- {{ age }}
toRefs 解构出 person的child的son的name ----- {{ child.son.name }}
toRefs 解构出 person的name ----- {{ name }}
toRefs 解构出 person的age ----- {{ age }}
toRefs 解构出 person的child的son的name ----- {{ child.son.name }}
修改person的name 修改person的age person的child的son的name新组件
Fragment
在vue2中模板标签内必须包裹一层根标签,vue3中则不需要。vue3会为多个跟标签包裹一层Fragment。这是写法上的优化。前面很多例子的代码中我都包裹了一层根标签,这是由于我的编辑器的eslint的问题,去掉根标签也可以正常运行。
有根标签的编译结果
Teleport
Teleport 组件的功能是将元素渲染到任意的页面位置中,直接扣过来官网的例子。
下列代码主要表达的是:点击按钮将弹框插入到 body 标签下
ModalButton.vue
Open full screen modal! (With teleport!) I'm a teleported modal! (My parent is "body") Close import { ref } from 'vue' export default { name: 'modal-button', setup() { const modalOpen = ref(false) return { modalOpen, } }, } .modal { position: absolute; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; align-items: center; justify-content: center; } .modal div { display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: white; width: 300px; height: 300px; padding: 5px; }
App.vue
App
import ModalButton from './ModalButton.vue' export default { setup() { return {} }, components: { ModalButton, }, }Suspense
Suspense 组件用于将异步组件包裹,提供一个过渡UI在异步完成之前。
Suspense 组件提供两个插槽:
- #default 默认插槽 存放异步组件
- #fallback 备用插槽 存放过渡UI
异步组件:
-
带有异步 setup() 钩子的组件。这也包含了使用 时有顶层 await 表达式的组件。
-
defineAsyncComponent
App.vue
App
加载中.... import Demo from './Demo.vue'Demo.vue
const res = await new Promise((resolve) => { setTimeout(() => { resolve({ name: 'zs', age: 12, sex: '男' }) }, 1000) })
异步组件
{{ res }}
div { padding: 10px; background: salmon; border: 1px solid #ccc; }组合式函数
组合式api的优点之一式将单个功能代码组合在一起,如果是可以复用的逻辑,那么可以抽离为一个组合式函数或者称为自定义hook,在需要该逻辑的地方导入即可
例子:提供一个组合函数,此函数在当前组件中监听鼠标移动事件,并将坐标显示出来,组件卸载前清掉事件。
App.vue
App
销毁子组件1 销毁子组件2
import { ref } from 'vue' import Demo from './Demo.vue' import Demo2 from './Demo2.vue' const Demo1Visible = ref(true) const Demo2Visible = ref(true)Demo1.vue
子组件1
x坐标为 {{ x }}, y坐标为{{ y }}
import useMouse from './mouse' const { x, y } = useMouse('.demo_1') .demo_1 { height: 100px; background: salmon; }Demo2.vue
子组件2
x坐标为 {{ x }}, y坐标为{{ y }}
import useMouse from './mouse' const { x, y } = useMouse('.demo_2') .demo_2 { height: 100px; background: salmon; }全局的api及指令的变动
API 参考 | Vue.js,大家先自行参考,后续深入学习时再进行更新。
结语
- 如果想在 vue3 中使用 element,请下载 element-plus
- vue3 的文档是最全的。Vue.js - 渐进式 JavaScript 框架 | Vue.js