Vue3
Vue3
你别睡这么晚Vue3
Vue3 的变化
性能的提升
- 打包大小减少 41%
- 初次渲染快 55%, 更新渲染快 133%
- 内存减少 54%
源码的升级
- 使用 Proxy 代替 defineProperty 实现响应式
- 重写虚拟 DOM 的实现和 Tree-Shaking
拥抱 TypeScript
- Vue3 可以更好的支持 TypeScript
新的特性
- Composition API(组合 API)
- setup 配置
- ref 与 reactive
- watch 与 watchEffect
- provide 与 inject
- ……
- 新的内置组件
- Fragment
- Teleport
- Suspense
- 其他改变
- 新的生命周期钩子
- data 选项应始终被声明为一个函数
- 移除 keyCode 支持作为 v-on 的修饰符
- ……
创建 Vue3 工程
vite 和 vue-cli 对比
vite | vue-cli | |
---|---|---|
支持的 vue 版本 | 仅支持 vue3.x | 支持 2.x 和 3.x |
是否基于 webpack | 否 | 是 |
运行速度 | 快 | 较慢 |
功能完整度 | 小而巧 | 大而全 |
是否建议企业级开发使用 | 暂不建议 | 建议 |
使用 vue-cli 创建
1 | ### 查看 @vue/cli 版本,确保 @vue/cli 版本在4.5.0以上 |
使用 vite 创建
- vite:新一代前端构建工具
- 优势:
- 开发环境中,无需打包操作,可快速冷启动(webpack 每次运行项目都要打包)
- 轻量快速的热重载 HMR(更改代码局部刷新,webpack 也行,但 vite 更轻量)
- 真正的按需编译,无需等待整个应用编译完成
- 传统构建 与 vite 构建对比(vite 现用现分析,按需导入,因此项目启动更快)
1 | npm init vite-app 项目名称 |
Vue3 项目结构
Vue3 中 main.js
代码有所改变:
1 | // 不再引入 Vue 构造函数,而是引入 createApp 工厂函数 |
Vue3 支持定义多个根节点,组件的 <template>
支持定义多个根节点:
1 | <template> |
常用 Composition API
setup
- setup 是 Vue3 中一个新的配置项,值为函数
- 组件中使用的数据、方法等都要配置在 setup 中
- setup 函数两种返回值:
- 返回一个对象,对象中的属性、方法可在模板中直接使用
- 返回一个渲染函数,可自定义渲染内容
- setup 函数的参数:
- props:值为对象,包含了组件外部传进来,且组件内部声明接收的属性
- context:上下文对象
attrs
:值为对象,包含了组件外部传进来,且组件内部没有声明接收的属性,相当于this.$attrs
slots
:收到的插槽内容,相当于this.$slots
emit
:触发自定义事件的函数,相当于this.$emit
1 | // 没错,渲染函数就叫 h |
注意:
- setup 在
beforeCreate
钩子之前执行,this
为undefined
- setup 不要和 Vue2 配置混用。Vue2 的配置可以访问到 setup 的属性方法,反过来不行;如有重名,setup 优先
- setup 不能是 async 函数,因为 async 函数返回的是 promise 不是对象,会导致模板无法访问属性方法
- 若要返回 promise 实例,需要
Suspense
和异步组件的配合
ref 函数
作用:定义响应式数据
语法:const name = ref(initValue)
ref
函数返回一个RefImpl
(reference implement) 实例对象,全称引用实现的实例对象- 它包含响应式数据,简称引用对象、reference 对象、ref 对象
- JS 访问数据:
name.value
- 模板访问数据:
<div>{{ name }}</div>
注意事项:
ref
函数可以接收基本数据类型和引用数据类型- 基本类型数据的响应式还是靠
Object.defineProperty()
完成 - 对象类型数据使用 ES6 的 Proxy 实现响应式,Vue3 把相关操作封装在
reactive
函数中 - 按照之前的办法,对于对象数据,应该遍历每一层的属性添加
getter
、setter
,但 Vue3 使用 Proxy 把内部数据一口气监测了
1 | <h2>{{ name }}</h2> |
1 | import { ref } from 'vue' |
reactive 函数
- 定义引用类型的响应式数据,不可用于 jibenleixingshuju
const 代理对象 = reactive(源对象)
接收对象或数组,返回代理对象(Proxy 的实例对象)reactive
的响应式是深度的- 基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据
1 | import { reactive } from 'vue' |
ref VS reactive
定义数据:
- ref 用于定义基本类型数据
- reactive 用于定义对象或数组类型数据
- ref 也可定义对象或数组类型数据,内部通过 reactive 转为代理对象
- 一般使用 reactive 函数,可以把所有数据封装为一个对象
原理:
- ref 通过
Object.defineProperty()
实现响应式 - reactive 通过 Proxy 实现响应式,Reflect 操作源对象数据
使用角度:
- ref 定义数据,访问数据需要
.value
,模板中不需要 - reactive 定义的数据,都不需要
Vue3 响应式原理
1 | let originPerson = { |
computed 函数
1 | import { reactive, computed } from 'vue' |
watch 函数
:::tip Vue3 watch
能侦听的东西
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
:::
1 | import { ref, reactive, watch } from 'vue' |
侦听 ref 定义的响应式数据:
- 注意不要写成
sum.value
1 | // 参数:侦听的数据,回调,其他配置 |
侦听多个 ref 定义的响应式数据:
1 | // newVal,oldVal 也是数组 |
侦听 ref 定义的对象类型数据:
1 | // 用 ref 定义对象类型数据 |
侦听 reactive 函数直接返回的那一整坨响应式数据:
- oldVal 是错误的!和 newVal 的值一样
- 强制开启了深度侦听,
deep
配置不生效!
1 | watch( |
侦听 reactive 定义的响应式数据某个属性:
- 如果是
() => person.info
oldVal 也是错误的! () => person.name
oldVal 是正确的,何时对何时错自己琢磨吧!- 此处没有强制开启深度监听
1 | // 如果监视的属性还是对象,则需要开启深度监听 |
侦听 reactive 定义的响应式数据多个属性:
1 | watch( |
watchEffect 函数
watchEffect
不需要指明监听哪个属性,回调里用到哪个属性,就自动监听哪个属性computed
注重计算的值,即回调函数的返回值,因此必须有返回值watchEffect
更注重过程,即回调函数的函数体,因此可没有返回值watchEffect
没有开启深度监听,也不能开启深度监听!watchEffect
内部自行修改数据,不会重新调用回调,因此不会出现递归调用
1 | // 回调中用到的数据只要发生变化,则直接重新执行回调 |
生命周期
注意和 vue2.x
的生命周期图作对比,beforeDestroy
和 destroyed
变为 beforeUnmount
和 unmounted
。
Vue3 也提供了 Composition API 形式的生命周期钩子,与 Vue2 中钩子对应关系如下:
beforeCreate
===>setup()
created
=======>setup()
beforeMount
===>onBeforeMount
mounted
=======>onMounted
beforeUpdate
===>onBeforeUpdate
updated
=======>onUpdated
beforeUnmount
==>onBeforeUnmount
unmounted
=====>onUnmounted
若和配置项生命钩子一起使用,则组合式会比配置项的先执行,如 onBeforeMount
先于 beforeMount
1 | import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' |
hook 函数
- hook 是一个函数,把 setup 函数的 Composition API 进行了封装
- 类似 Vue2 的 Mixin,能复用代码,让 setup 里的逻辑更清晰
- hook 放在 hooks 文件夹中,一个文件对应一个功能模块,以
useXxx
命名
1 | // hooks/usePoint.js |
1 | // 使用 hook |
toRef 函数
- 创建一个 RefImpl 实例对象,其 value 值指向另一个对象的某个属性,修改 value 值会修改源对象对应的属性
- 应用:需要把响应式对象的某个属性单独提供给外部使用
- 批量创建:
toRefs
1 | import {reactive, toRef, toRefs} from 'vue' |
其它 Composition API
shallowReactive & shallowRef
shallowReactive
:只处理对象最外层属性的响应式,即浅响应式shallowRef
:基本数据类型和ref
相同,对象数据不再会调用reactive
,因此只有对象引用改变了才是响应式的- 若一个对象数据,结构很深,但只有最外层属性变化,可用
shallowReactive
- 若一个对象数据,属性不会改变,而是使用新对象替换,可用
shallowRef
1 | import { shallowReactive, shallowRef } from 'vue' |
readonly & shallowReadonly
readonly
: 让一个响应式数据变为只读的(深只读)shallowReadonly
:让一个响应式数据变为只读的(浅只读)- 应用场景: 不希望数据被修改时,如你用了别人的响应式数据,但是别人不希望你修改时
1 | setup() { |
toRaw & markRaw
toRaw
:
- 将一个由
reactive
生成的响应式对象转为普通对象 - 用于读取响应式对象对应的普通对象,对该普通对象的操作不会引起页面更新
markRaw
:
- 标记一个对象,让其不成为响应式对象
- 有些值不应设置为响应式,比如复杂的第三方库
- 当渲染复杂且不变的数据时,跳过响应式转换可提高性能
注意:仅仅让数据变为非响应式的,数据变的依旧变,只是没引起页面更新
1 | setup() { |
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
1 | <input type="text" v-model="keyword" /> |
1 | import { ref, customRef } from 'vue' |
provide / inject
实现祖先组件与后代组件之间通信。
1 | // 祖先组件传递数据 |
1 | // 后代组件接收数据 |
响应式数据的判断
isRef
: 检查一个值是否为一个ref
对象isReactive
: 检查一个对象是否是由reactive
创建的响应式代理isReadonly
: 检查一个对象是否是由readonly
创建的只读代理isProxy
: 检查一个对象是否是由reactive
或者readonly
方法创建的代理
Compositon API 的优势
Options API 存在的问题
使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data,methods,computed 等地方修改。
Composition API 的优势
可以更加优雅地组织代码、函数,让相关功能的代码更加有序的组织在一起。说白了就是让同一个功能的代码整合到一起,日后修改代码直接找对应的功能模块。
新的组件
Fragment
- 在 Vue2 中: 组件必须有一个根标签
- 在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个
Fragment
虚拟元素中 - 好处: 减少标签层级, 减小内存占用
Teleport
- 将包裹的 HTML 结构移动到指定元素的末尾
to
属性为 CSS 选择器
简易的模态框效果:
1 | <teleport to="#root"> |
1 | .mask { |
Suspense
等待异步组件时渲染额外内容,让用户体验更好
异步引入组件:
1 | import { defineAsyncComponent } from 'vue' |
使用 Suspense
包裹组件,实际上是往插槽填充内容,default
插槽填充组件内容,fallback
插槽填充组件加载时显示的内容:
1 | <Suspense> |
另外,若 Child
组件的 setup
函数返回一个 Promise 对象,也能渲染 fallback
里的内容:
1 | async setup() { |
其他改变
- 全局 API 的转移
Vue3 将全局的 API,即:Vue.xxx
调整到应用实例 app
上:
Vue2 全局 API | Vue3 实例 API |
---|---|
Vue.config.xxx | app.config.xxx |
Vue.config.productionTip | 移除 |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
data
选项应始终被声明为一个函数过渡类名的更改:
1 | /* Vue2 */ |
- 移除
keyCode
作为v-on
的修饰符,同时也不再支持config.keyCodes
- 移除
v-on.native
修饰符,子组件没有在emits: ['close']
声明的自定义事件作为原生事件处理 - 移除过滤器
filter
- …
组件上的 v-model
当需要维护组件内外数据的同步时,可以在组件上使用 v-model
指令。
父组件传值:
1 | <!-- 父组件传值 --> |
子组件在 emits
节点声明自定义事件,格式为 update:xxx
,调用 $emit
触发自定义事件:
1 | export default { |
注意,在 vue3
中 props
属性同样是只读的,上面 this.number++
并没有修改 number
的值。
其实通过 v-bind
传值和监听自定义事件的方式能实现和 v-model
相同的效果。
EventBus
借助于第三方的包 mitt
来创建 eventBus
对象,从而实现兄弟组件之间的数据共享。
安装 mitt
依赖包:
1 | npm install mitt@2.1.0 |
创建公共的 eventBus
模块:
1 | import mitt from 'mitt' |
数据接收方调用 bus.on()
监听自定义事件:
1 | import bus from './eventBus.js' |
数据接发送方调用 bus.emit()
触发事件:
1 | import bus from './eventBus.js' |
vue 3.x 全局配置 axios
实际项目开发中,几乎每个组件中都会使用 axios
发起数据请求。此时会遇到如下两个问题:
- 每个组件中都需要导入
axios
(代码臃肿) - 每次发请求都需要填写完整的请求路径(不利于后期的维护)
在 main.js
文件中进行配置:
1 | // 配置请求根路径 |
组件调用:
1 | this.$http.get('/users') |