Vue3
Vue3 是由尤雨溪等99位贡献者开发的一款前端框架。于 2020/09/18 正式发布,耗时2年多、2600+次提交、30+个RFC、600+次PR,在这里,框架将授予你「声明式」、「组件化」的编程模型,导引「响应式」之力。你将扮演一位名为「CV工程师」的神秘角色在自由的编码中邂逅样式各异、功能独特的组件库们,和他们一起解决问题,完成多样的需求——同时,逐步发掘「组合式API」的真相。
注意:尽管 Vue3 兼容大部分 Vue2 写法,但组合式 API 写法下,最好不要混用
Vue-cli创建工程
Vue3 推荐使用 Vite 来创建工程,但刚学完 vue2 还是接着用 Vue-cli 把 Vue3 新的语法学完,再去学习 Vite 和 pinia 的使用吧
工程结构和 vue2 中的一样,但文件内有些变化:
1 | // 引入的不再是Vue构造函数 |
单文件组件模板可以没有根元素
1 | <template> |
初识setup
setup 是 Vue3 中一个新的配置项,值为一个函数。
组件中所有的数据、方法都要配置在 setup 中
setup函数有两种返回值
(1) 返回一个对象,对象中的所有属性都可以在组件模板中使用
(2) 返回一个渲染函数,会替换掉组件模板(基本用不到)
setup 执行是在创建实例之前,也就是 beforeCreate (之前)执行,所以 setup 函数中的 this 不是组件的实例,而是undefined,setup是同步的
1 | <template> |
< script setup >
在 setup()
函数中手动暴露大量的状态和方法非常繁琐。可以通过构建工具来简化该操作。
给 <script>
标签加上 setup 属性,里面的代码会被编译成 setup()
函数的内容
与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行
1 | <template> |
setup带来的改变:
- 解决了vue2的data和methods方法相距太远,无法组件之间复用
- 提供了script标签引入共同业务逻辑的代码块,顺序执行
- script变成setup函数,默认暴露给模版
- 组件直接挂载,无需注册
- 自定义的指令也可以在模版中自动获得
- this不再是这个活跃实例的引用
- 带来的大量全新api,比如defineProps,defineEmits,withDefault,toRef,toRefs
响应式
对比探究 Vue3 中的响应式使用与实现
ref函数
setup()
返回的数据并没有自带响应式的效果,在 Vue3 中要实现响应式,需要使用 ref()
对数据进行处理
作用:用 ref()
定义响应式变量,将传入的值包装为一个带 value 属性的 ref 对象(引用实现对象),允许我们创建可以使用任何值类型的响应式数据
JS 中需要 .value 才能获取或修改数据,模板中直接写 ref 对象名
1 | <template> |
ref()
传入值为基本数据类型时,其响应式本质和 vue2 一样,是通过 Object.defineProperty()
的 getter 和 setter 进行数据劫持和数据代理 实现的
1 | RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 'chuckle', _value: 'chuckle'} |
ref()
传入值为对象类型时,会去调用 reactive()
处理 value,而 reactive()
返回一个 Proxy 类型的对象
暂时看不懂的东西:
1 | RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: {…}, _value: Proxy(Object)} |
reactive函数
reactive()
传入一个对象类型,返回该对象类型的代理对象(Proxy的实例对象,简称proxy对象)
reactive()
仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效
proxy 对象直接用变量名访问数据,无需 .value
可以直接通过下标响应式地修改数组数据,这是通过 defineProperty 实现的响应式所做不到的
1 | <template> |
在定义响应式数据时,通常基本数据类型用 ref()
,引用数据类型用 reactive()
但由于 ref()
定义的响应式数据需要 .value 才能获取和修改数据值,所以也通常将基本数据类型扔进一个对象中,然后用 reactive()
,可以省去 .value
Vue2的响应式原理
依靠 Object.defineProperty()
实现原理:
- 基本数据类型:通过 defineProperty 进行数据劫持与代理
- 对象类型:深度遍历对象,通过 defineProperty 进行数据劫持与代理
- 数组类型:通过重写更新数组的一系列方法(push等)
存在问题:
- 深度遍历对象存在性能与效率问题
- 对象新增、删除属性,无法响应式,需用 $set 和 $delete
- 直接通过下标修改数组,无法响应式
Vue3的响应式原理
依靠 ES6 中新的 API window.Proxy()
构造函数,解决了 Vue2 响应式中的问题
Proxy()
返回 proxy 对象,它可以代理对源数据中任何属性的任何操作
Proxy()
传入两个参数,第一个:要代理的源数据,第二个:一个配置对象,其中有 get、set 等方法,用于设置拦截
如果Proxy的第二个参数(配置对象)没有设置任何拦截,就等同于直接访问原对象
尝试使用 Proxy()
实现代理数据操作:
1 | // 源数据 |
注意:后续在 proxy 对象上添加新属性也是响应式的
虽然上面已经模仿实现了”响应式”,但 Vue3 中还用到了 window.Reflect
对象
Proxy()
用于代理(代理对数据的操作),Reflect
用于反射(实际对数据的操作)
Reflect 用法:
Reflect.get(target, prop)
获取某个对象(target)中某个属性(prop)的值Reflect.set(target, prop, value)
修改某个对象中某个属性的值为valueReflect.deleteProperty(target, prop)
删除某个对象中的某个属性
Reflect 身上还有很多方法,ECMA 也在将原本 Object 中的方法移植到 Reflect 中
1 | // 源数据 |
总结 Vue3 响应式的实现原理:
- 通过Proxy(代理):拦截对象中任意属性的变化, 包括增删改查等。
- 通过Reflect(反射):对源对象的属性进行实际操作。
props声明
和 Vue2 一样,一个组件需要显式声明它所接受的 props
<script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明
1 | <script setup> |
没有使用 <script setup>
的组件中,prop 可以使用 props 选项来声明:
1 | export default { |
props 也是一个 proxy 对象,也能实现响应式
注意:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,不应该在子组件中去更改一个 prop。
context上下文
context 是 setup(props, context)
的第二个参数,代表上下文
context 中有一些属性和方法
- attrs 没有接收的 prop 都会出现在里面
emit()
触发父组件绑定来的自定义事件- slots 没有接收的 slot 都会出现在里面
<script setup>
中 useContext() 已经废弃
需要使用独立的 API 获取原本 context 中的属性和方法:
useAttrs()
没有接收的 prop 都会出现在里面defineEmits()
接收父组件绑定的自定义事件useSlots()
获取父组件中插槽传递的所有虚拟Dom对象,按插槽名字区分
需要引入后使用:
1 | import { useAttrs,defineEmits, useSlots } from 'vue'; |
1、useAttrs()
1 | <!-- 父组件 --> |
2、defineEmits()
1 | <!-- 父组件 --> |
3、useSlots()
1 | <!-- 父组件 --> |
defineExpose函数
子组件通过 defineExpose()
向父组件暴露数据
当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)
1 | <!-- 父组件 --> |
computed计算属性
computed()
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
1 | import { computed } from 'vue'; |
watch函数
watch()
监视一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
返回值是一个用来停止监听的的函数
基本使用:
1 | watch(refObj, (newValue, oldValue)=>{ |
1、可以同时监视多个响应式数据,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
1 | watch([name, age], ([newName, newAge], [oldName, oldAge]) => {}) |
2、当监听一个 reactive 定义的 proxy 响应式对象时,侦听器会强制启用深层模式,但回调函数收到的新值和旧值参数将是同一个对象
1 | let person = reactive({ |
3、监听 proxy 对象中某一个基本数据类型的属性,新值和旧值可以正常获取,但监听对象需写成函数返回值形式
1 | let person = reactive({ |
4、监听 proxy 对象中某一个对象类型的属性,新值和旧值是同一个对象
1 | let person = reactive({ |
5、若监视一个使用 ref()
定义的对象类型的响应式数据,则需要 deep: true 开启深度监视,或去监视其 .value(此时value是一个proxy对象,会自动开启深度监视)
1 | let person = ref({ |
watchEffect函数
watchEffect()
立即运行一个回调函数,同时响应式地追踪其依赖,并在依赖更改时重新执行回调
返回值是一个用来停止监听的的函数
1 | const count = ref(0) |
和计算属性有些像,计算属性重在最后的返回值,而watchEffect重在逻辑过程,而返回值确定
Vue3生命周期
Vue3 生命周期钩子名称改为了 on 开头,但作用和 Vue2 中的差不多,但也添加了一些新钩子,有需要在文档中查看用法
Vue3 生命周期图:
自定义hook
hook 本质是一个函数。
自定义hook,就是将 setup 中使用过的组合式API进行封装,写在一个单独的 JS 文件中并暴露出去,实现代码复用,命名通常以 use 开头,集中放在 hooks 文件夹中
作用:在 Vue3 中通过组合式函数实现 JS 代码复用,而避免使用 mixin 混入
组合式函数:是一个利用 Vue 的组合式 API 来封装和复用有状态和相关逻辑的函数。
案例:写一个实时返回鼠标坐标的 hook,让任何组件引入即可使用
1 | import { reactive, onMounted, onBeforeUnmount } from "vue" |
组件中展示鼠标坐标,非常方便,引入hook并执行,获取状态,然后使用状态
1 | <template> |
toRef与toRefs
toRef()
可以将值、refs 或 getters 规范化为 refs
也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然
1 | let refObj = toRef(proxyObj, 'propName'); |
为什么要用 toRef()
:
当直接解构 proxy 响应式对象时,对于属性值是基本数据类型的属性,传的是值,而非引用地址,解构后的变量会失去响应式的功能
1 | <template> |
使用 toRef()
可以不用重新将解构的值通过 ref()
创建一个独立的 ref 对象,而是直接用源对象中的属性,通过引用关系,创建一个对应的 ref 对象,与其源属性保持同步
1 | let obj = reactive({ |
toRefs()
功能与 toRef()
差不多,但可以批量处理某个 proxy 对象中的所有属性(浅层),返回一个对象,对象中存着源对象属性对应的 ref 对象
1 | let refArr = toRefs(proxyObj); |
总结:toRef()
创健一个 ref 对像,其 value 值指向另一个对象中的某个属性
其它组合式API
介绍较不常用的组合式API
响应式数据的判断
1、isRef()
检查某个值是否为 ref 对象
2、unref()
如果参数是 ref,则返回其 value 值,否则返回参数本身
2、isProxy()
检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理
3、isReactive()
检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理
浅层响应式
shallowReactive()
reactive() 的浅层作用形式,只对浅层属性进行响应式处理
shallowRef()
ref() 的浅层作用形式,只有浅层的 value 是响应式的
只读API
readonly()
深层的只读,接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理
shallowReadonly()
浅层的只读
toRaw、markRaw
toRaw()
根据一个 Vue 创建的 proxy 代理对象返回其原始对象的引用地址
markRaw()
将一个对象标记为不可被转为 proxy 代理。返回该对象本身。可以让某些数据追加到 proxy 对象上后没有响应式功能。
自定义ref
customRef()
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象
track()
追踪数据变化,trigger()
重新解析模板
作用:用于防抖、延迟更新视图等需求
1 | import { customRef } from 'vue' |
依赖注入
provide()
祖先组件提供一个值,可以被后代组件注入。接受两个参数:第一个参数是要注入的 key(名字),第二个参数是要注入的值
inject()
注入一个由祖先组件或整个应用 (app.provide()) 提供的值。
通过这两个 API 可以实现祖先与后代组件的通信,非常方便
1 | // 祖先组件 |
注意:provide()
、inject()
必须在组件的 setup() 阶段同步调用
Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,inject() 将返回 undefined,除非提供了一个默认值。
Fragment组件
在 Vue2 中,组件模板必须有一个根标签
在 Vue3 中,若模板中没有根标签,会将模板包含在一个 fragment 虚拟元素中,不会被实际渲染出来
Teleport组件
<teleport to="">
可以将组件模板、插槽移动到指定的任意元素中(尾插),to 中写 css 选择器
一个简单的弹窗组件:
1 | <template> |
Suspense组件(实验)
作用:异步地动态加载子组件,父组件加载完就直接显示,不等待异步的子组件
将要异步加载的子组件使用 <Suspense>
包裹
<Suspense>
提供两个有名插槽,default 最终要加载的子组件,fallback 子组件还未加载出来时展示的模板
1 | <template> |
除了 <Suspense>
还可以使用 defineAsyncComponent()
异步引入组件
1 | import { defineAsyncComponent } from 'vue' |
异步引入组件后:
(1) 该组件的 setup() 就可以是 async 函数,可以用 await,可以返回 promise 对象。
(2) <script setup>
中也可以使用顶层 await。结果代码会被编译成 async setup()
Vue3的其它变化
1、全局 API 的转移:Vue.xxx
调整到应用实例 app
上
2.x 全局 API(Vue ) |
3.x 实例 API (app ) |
---|---|
Vue.config.xxxx | app.config.xxxx |
Vue.config.productionTip | 移除,因为不再有生产提示 |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
Vue.prototype | app.config.globalProperties |
2、Vue动画中过度类名的更改:
1 | .v-enter, |
1 | .v-enter-from, |
3、keyCode 不再作为 v-on 的修饰符,同时也不再支持 config.keyCodes
4、移除 v-on.native
修饰符
5、移除过滤器 filter
6、父组件给子组件绑定自定义事件,子组件中需要接收才能使用,
(1) defineEmits()
接收父组件绑定的自定义事件
(2) 或者选项式中的 emits 配置项
THE END