简介
- 2020年9月18日发布Vue3正式版本V3.0.0,命名为One Piece。
- Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。
该选哪一个?
两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的!关于 Vue 的基础概念和知识在它们之间都是通用的。
选项式 API 以“组件实例”的概念为中心 (即上述例子中的 this),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致。同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好。
组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要你对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。
[图片上传失败...(image-a9c136-1660454836197)]
Vue3带来的变化
1. 性能提升1.3~2.x
- 核心代码 + Composition API :13.5kb,最小可以到11.75kb
- 所有的Runtime:22.5kb(Vue2是32kb)
为什么会有这么大的性能提升呢? 这里就要说到 Compiler 的原理: - 静态Node不再做更新处理
- 静态绑定的class和id不再做更新处理
- vue在mount的过程中会编译成ast语法树, 会给动态的内容打上一个标记
PatchFlag
,进行更新分析(动态绑定),会区分哪些是静态内容哪些是动态内容,然后对动态内容去做更新处理 - 事件监听器Cache缓存处理(cacheHandlers),组件创建的过程中不会去重复的多次实例化,对内存的优化是非常好的,减少创建对象的数量,从而减少内存占用提高性能
- hoistStatich自动针对多静态节点进行优化,输出字符串
测试地址:https://vue-next-template-explorer.netlify.app
2. Ts支持,新增:Fragment、Teleport、Suspense
- Fragment不受根节点限制,渲染函数可接收Array。意思就是我们在temeplate中不再受根节点限制,可以任意的插入多个文本,字符串或者图片
- Teleport--类似Portal,随用随取比如:弹窗、Actions,比如我们有可能需要在app节点之外比如body中控制一个弹窗的显示或者隐藏,可以用到它
- Suspense 从框架层面的一个异步组件,可以支持嵌套加载的一个场景,比如树形组件我们要加载多层加的组织架构,我们希望把下面所有的组织架构都加载完成以后再显示整个组件,这个时候就需要用到它。例如:
async setup()
3. 按需加载(配合vite)& 组合Api
- 官方文:https://v3.cn.vuejs.org
- 组合式Api: https://composition-api.vuejs.org/api.html
Vue2和Vue3的比较
1. 为什么要用 Composition API?
(1) Vue2对于复杂逻辑的组件,后期变得无法维护
下面是vue2实现加减的代码:
count: {{ count }}
倍数: {{ multiple }}
上面代码只是实现了对 count 的加减以及显示倍数, 就需要分别在data、methods、computed中进行操作,当我们增加一个需求,就会出现下图的情况:
[图片上传失败...(image-7fc1e4-1660454836197)]
当我们业务复杂了就会大量出现上面的情况, 随着复杂度上升,就会出现这样一张图:
[图片上传失败...(image-43cb6f-1660454836197)]
当这个组件的代码超过几百行时,这时增加或者修改某个需求, 就要在data、methods、computed以及mounted中反复的跳转
如果可以按照逻辑进行分割,将上面这张图变成下边这张图,是不是就清晰很多了呢, 这样的代码可读性和可维护性都更高:
[图片上传失败...(image-6d6040-1660454836197)]
那么vue2.x版本给出的解决方案就是Mixin, 但是使用Mixin会有缺陷:
- 命名空间冲突
- 不清楚暴露出来的变量的作用
- 逻辑重用到其他 component 经常遇到问题(不易复用)
Vue3.x就推出了 Composition API 主要就是为了解决上面的问题,将零散分布的逻辑组合在一起来维护,并且还可以将单独的功能逻辑拆分成单独的文件。
(2)scoped slot作用域插槽(配置项多、代码分裂、性能差)
(3) Vue2对Ts支持不充分
(4)Vue3使复杂组件逻辑进行分离,组件间的逻辑共享
(5)Vue3组合式API + 函数式编程
组合式 API (Composition API)
- 通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与
搭配使用。这个
setup
attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,中的导入和顶层变量/函数都能够在模板中直接使用。
[图片上传失败...(image-fcfc2-1660454836197)]
1. setup
-
setup
是 Vue3.x 新增的一个选项, 他是组件内使用Composition API
的入口。
export default defineComponent({
beforeCreate() {
console.log("----beforeCreate----");
},
created() {
console.log("----created----");
},
setup() {
console.log("----setup----");
},
})
[图片上传失败...(image-cf7909-1660454836197)]
- 通过代码打印结果,
setup
执行时机是在 beforeCreate 之前执行
setup 参数
- 使用setup时,它接受两个参数:
- props: 组件传入的属性
- context
- setup 中接受的props是响应式的, 当传入新的 props 时,会及时被更新。由于是响应式的, 所以不可以使用 ES6 解构,解构会消除它的响应式。
- 错误代码示例, 这段代码会让 props 不再支持响应式:
export default defineComponent ({
setup(props, context) {
const { name } = props
console.log(name)
},
})
- 开发中我们想要使用解构,还能保持
props
的响应式,需要用到后面的toRefs
来解决 -
setup
中不能访问 Vue2 中最常用的this
对象,所以context
中就提供了this中最常用的三个属性:attrs
、slot
和emit
分别对应 Vue2.x 中的$attr
属性、slot
插槽 和$emit
发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值。
2. reactive、ref 与 toRefs
- 在 vue2.x 中, 定义数据都是在data中, 但是 Vue3.x 可以使用reactive和ref来进行数据定义。
import { ref } from 'vue'
const count = ref(0)
reactive
函数可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等
第 {{ year }} 年
姓名: {{ nickname }}
年龄: {{ age }}
上面的代码中,我们绑定到页面是通过user.name
, user.age
;这样写感觉很繁琐,我们能不能直接将user
中的属性解构出来使用呢? 答案是不能直接对user
进行结构, 这样会消除它的响应式, 这里就和上面我们说props不能使用 ES6 直接解构就呼应上了。那我们就想使用解构后的数据怎么办,解决办法就是使用toRefs
。
toRefs
用于将一个 reactive
对象转化为属性全部为 ref
对象的普通对象。具体使用方式如下:
第 {{ year }} 年
姓名: {{ nickname }}
年龄: {{ age }}
生命周期钩子
[图片上传失败...(image-e57ed6-1660454836197)]
从图中我们可以看到 Vue3.0 新增了setup
,这个在前面我们也详细说了, 然后是将 Vue2.x 中的beforeDestroy
名称变更成beforeUnmount
; destroyed
表更为 unmounted
,作者说这么变更纯粹是为了更加语义化,因为一个组件是一个mount
和unmount
的过程。其他 Vue2 中的生命周期仍然保留。
上边 生命周期图 中并没包含全部的生命周期钩子, 还有其他的几个, 全部生命周期钩子如图所示:
[图片上传失败...(image-e1eadd-1660454836197)]
我们可以看到 beforeCreate
和 created
被 setup
替换了(但是Vue3中你仍然可以使用, 因为Vue3是向下兼容的, 也就是你实际使用的是vue2的)。其次,钩子命名都增加了 on
; Vue3.x还新增用于调试的钩子函数 onRenderTriggered
和 onRenderTricked
自定义 Hooks
开篇的时候我们使用 Vue2.x 写了一个实现加减的例子, 这里可以将其封装成一个 hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。 useCount.ts 实现:
import { ref, Ref, computed } from "vue";
type CountResultProps = {
count: Ref;
multiple: Ref;
increase: (delta?: number) => void;
decrease: (delta?: number) => void;
};
export default function useCount(initValue = 1): CountResultProps {
const count = ref(initValue);
const increase = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value += delta;
} else {
count.value += 1;
}
};
const multiple = computed(() => count.value * 2);
const decrease = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value -= delta;
} else {
count.value -= 1;
}
};
return {
count,
multiple,
increase,
decrease,
};
}
接下来看一下在组件中使用useCount这个 hook:
count: {{ count }}
倍数: {{ multiple }}
简单对比 vue2.x 与 vue3.x 响应式
Vue3.x 将使用 Proxy
取代 Vue2.x 版本的 Object.defineProperty
这里就简单对比一下:
-
Object.defineProperty
只能劫持对象的属性, 而Proxy
是直接代理对象
由于Object.defineProperty
只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy
直接代理对象, 不需要遍历操作
-
Object.defineProperty
对新增属性需要手动进行Observe
因为Object.defineProperty
劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty
进行劫持。也就是 Vue2.x 中给数组和对象新增属性时,需要使用set内部也是通过调用Object.defineProperty
去处理的
Teleport
Teleport
就像是哆啦 A 梦中的「任意门」,任意门的作用就是可以将人瞬间传送到另一个地方。有了这个认识,我们再来看一下为什么需要用到 Teleport 的特性呢,看一个小例子:
- 在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。
- Dialog从用户感知的层面,应该是一个独立的组件,从 dom 结构应该完全剥离 Vue 顶层组件挂载的 DOM;同时还可以使用到 Vue 组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。
- 此时就需要
Teleport
上场,我们可以用
包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。 - 接下来就举个小例子,看看 Teleport 的使用方式:
我们希望 Dialog 渲染的 dom 和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素:
定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致:
{{ title }}
最后在一个子组件Header.vue中使用Dialog组件
...
[图片上传失败...(image-8ff413-1660454836197)]
可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.
Suspense
Suspense是 Vue3.x 中新增的特性, 那它有什么用呢?我们通过 Vue2.x 中的一些场景来认识它的作用。 Vue2.x 中应该经常遇到这样的场景:
...
加载中...
在前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。
如果你使用过vue-async-manager
这个插件来完成上面的需求, 你对Suspense
可能不会陌生,Vue3.x 感觉就是参考了vue-async-manager
.
Vue3.x 新出的内置组件Suspense
, 它提供两个template
slot, 刚开始会渲染一个 fallback
状态下的内容, 直到到达某个条件后才会渲染 default
状态的正式内容, 通过使用Suspense
组件进行展示异步渲染就更加的简单。注意如果使用 Suspense
, 要返回一个 promise
使用:
Loading...
asyncComponent.vue:
这个是一个异步加载数据
用户名:{{user.nickname}}
年龄:{{user.age}}
从上面代码来看,Suspense
只是一个带插槽的组件,只是它的插槽指定了default
和 fallback
两种状态。
片段(Fragment)
在 Vue2.x 中, template中只允许有一个根节点:
但是在 Vue3.x 中,你可以直接写多个根节点