前提条件:
node.js 15.0以上版本
vscode 17+版本
vscode添加插件
Vue Language Features (Volar)
TypeScript Vue Plugin (Volar)
并且禁用Vue2的Vetur插件
> npm init vue@latest
一、Vue2与Vue3的区别
1. 生命周期:
整体上变化不大,只是大部分生命周期钩子名称上 + “on”,功能上是类似的。不过有一点需要注意,Vue3 在组合式API(Composition API,下面展开)中使用生命周期钩子时需要先引入,而 Vue2 在选项API(Options API)中可以直接调用生命周期钩子;
2. 多根节点:
template标签内部不再只允许有一个根节点,而是采用了多根节点的形式,即fragment
3. Composition API
Vue2采用Options API,文件比较大的时候,一个逻辑的内容会散乱的分布在不同的地方,导致可读性差。Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。
4. Teleport
Vue3 提供 Teleport 组件可将部分 DOM 移动到 Vue app 之外的位置。比如项目中常见的 Dialog 弹窗
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
<div class="dialog" v-if="dialogVisible"> 我是弹窗,我直接移动到了body标签下</div>
</teleport>
5. 响应式原理
Vue2.0
- 基于Object.defineProperty来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
- Object.defineProperty 无法检测到对象属性的添加和删除 。
- 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
- 深度监听需要一次性递归,对性能影响比较大。
- 劫持的是对象上的属性,新增元素需要再次添加Observer
Vue3.0
- 基于Proxy和Reflect,可以原生监听数组,可以监听对象属性的添加和删除。
- 不需要一次性遍历data的属性,可以显著提高性能。
- 因为Proxy是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。
- 劫持整个对象,不需要做特殊处理
- 必须修改代理对象才能触发视图更新,即Proxy实例
6. TypeScript的支持更好
7. diff算法
Vue2 双端比较 虚拟DOM全量比较
Vue3 去头尾的最长递增子序列算法,使用PatchFlag标记处理,没变化的值不参与渲染
在Vue2.0当中,当数据发生变化,它就会新生成一个DOM树,并和之前的DOM树进行比较,找到不同的节点然后更新。但这比较的过程是全量的比较,也就是每个节点都会彼此比较。但其中很显然的是,有些节点中的内容是不会发生改变的,那我们对其进行比较就肯定消耗了时间。所以在Vue3.0当中,就对这部分内容进行了优化:在创建虚拟DOM树的时候,会根据DOM中的内容会不会发生变化,添加一个静态标记。那么之后在与上次虚拟节点进行对比的时候,就只会对比这些带有静态标记的节点。
二、setup语法糖
基本用法
要使用这个语法,需要将 setup attribute 添加到 <script> 代码块上:
<script setup>
const a = ref(1);
console.log('hello script setup')
</script>
当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括声明的变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用,不再需要使用 return 导出。
相信使用过 vue3 的同学都有这个感受,所有在 <template> 中使用的变量,函数,都需要在 <script> 中显示 return 导出,不仅写起来麻烦,还有种多次一举的感受,单单这一点就可以节省大量的代码。
1 声明响应式状态
1.1 reactive使用
ps: 不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
state.count++
}
</script>
<template>
<button @click="increment">
{{ state.count }}
</button>
</template>
1.2 用ref()定义响应式变量
reactive有两个局限性
1.2.1 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型无效。
1.2.2 因为 Vue 的响应式系统是通过属性访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
reactive() 的种种限制归根结底是因为 JavaScript 没有可以作用于所有值类型的 “引用” 机制。为此,Vue 提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
1.3 使用TS为ref()标注类型
import { ref } from 'vue'
import type { Ref } from 'vue'
const year: Ref<string | number> = ref('2020')
year.value = 2020 // 成功!
1.4 ref 在模板中的解包
当 ref 在模板中作为顶层属性被访问时,它们会被自动“解包”,所以不需要使用 .value。下面是之前的计数器例子,用 ref() 代替:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- 无需 .value -->
</button>
</template>
1.5 ref响应式语法糖
注意:此功能为实验性功能,暂时不推荐使用
<script setup>
let count = $ref(0)
function increment() {
// 无需 .value
count++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
2. 组件的使用
- Vue2使用的组件的时候可以为组件定义name,而使用setup语法糖的时候,无法给组件提供名称。Vue3会根据文件名字自动推断组件名。
- 普通组件使用import引入之后,可以在页面直接使用,不需要再引用;如果实在想自定义组件名,可以使用多个script标签,其中一个使用Options API的形式来实现定义组件名
3. props的使用-defineProps
3.1 运行时声明。
<script setup lang='ts'>
const props = defineProps({
foo: String,
bar: {
type: Number,
required: true
}
})
</script>
在这里,运行时声明指对于 props 的类型的声明,这种声明方式 IED 是无法检测和给出提示的,只有在运行后才会给出提示,例如: 这是 options API 的 props 写法,也就是运行时声明。这样的写法 IDE 是无法检测到 props 是否按照类型进行传递,只能运行后才能检测到,因此这种叫运行时声明。
3.2 类型声明(※墙裂推荐)
<script setup lang='ts'>
const props = defineProps<{
foo?: string
bar: number
}>()
</script>
在这里类型声明指基于 ts 的类型检查,对 props 进行类型的约束,因此,要使用类型声明,需要基于 ts,即 <script setup lang="ts">
4. 默认值withDefaults
如果想给defineProps接收的值设置默认值,可以使用withDefaults()函数,需要两个入参
const props = withDefaults(defineProps<{
message: string,
list: User[]
}>(),{
message: "默认值",
list: () => []
})
这里如果使用解构赋值会使props失去响应式,想要保留响应式需要使用toRefs包裹props
const {message, list} = toRefs(props)
console.log(message,list)
5. 自定义时间-defineEmits
5.1 运行时声明
<script setup lang="ts">
// 这样是没有任何的类型检查的
const emit = defineEmits(['handleClick', 'handleChange']);
const handleClick = () => emit('handleClick', Date.now()+'');
const handleChange = () => emit('handleChange', Date.now());
</script>
5.2 类型声明
<script setup lang="ts">
interface Click {
id: string,
val: number,
}
// 完美的类型检查
// List.Basic 是基于 ts 自动扫描 types 文件夹以及 delcare namespace 自动导入的
const emit = defineEmits<{
(e: 'handleClickWithTypeDeclaration', data: Click): void,
(e: 'handleChangeWithTypeDeclaration', data: string): void,
}>();
const handleClickWithTypeDeclaration = () => emit('handleClickWithTypeDeclaration', { id: '1', val: Date.now() });
const handleChangeWithTypeDeclaration = () => emit('handleChangeWithTypeDeclaration', {
id: 1,
content: 'change',
isDone: false,
});
</script>
7. 依赖注入Provide和inject
都知道组件传值吧,在vue2中,如果要在后代组件中使用父组件的数据,那么要一层一层的父子组件传值或者用到vuex,但是现在,无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据
//父
import { provide } from 'vue'
setup(){
let fullname = reactive({name:'阿月',salary:'15k'})
provide('fullname',fullname) //给自己的后代组件传递数据
return {...toRefs(fullname)}
}
//后代
import {inject} from 'vue'
setup(){
let fullname = inject('fullname')
return {fullname}
}
三、pinia
为什么使用 pinia 呢,用起来的感觉和vuex相似,但是简单轻量,很顺滑。
- 不存在
mutations,最初是为了vue-devtools集成,但现在不需要了,现在actions同时支持同步和异步。 - 不需要进行复杂的配置来支持
typescript,一切都是类型化的,并且API的设计方式是尽可能利用TS类型推断。 - 不再有
modules的嵌套结构,你仍然可以在一个store中导入和使用其他一个store来隐式嵌套stores。
四、Type Script
1. 基础类型
string、number、boolean、Array、枚举、any、void、never、null和undefined、元组、Object、类型断言
2. 复杂类型
class、interface 一种定义复杂类型的格式, 比如我们用对象格式存储一篇文章, 那么就可以用接口定义文章的类型:
interface Article {
title: stirng;
count: number;
content:string;
fromSite: string;
}
const article: Article = {
title: '为vue3学点typescript(2), 类型',
count: 9999,
content: 'xxx...',
fromSite: 'baidu.com'
}
在这种情况下,当我们给article赋值的时候, 如果任何一个字段没有被赋值或者字段对应的数据类型不对, ts都会提示错误, 这样就保证了我们写代码不会出现上述的小错误.
非必填(?)
还是上面的例子, fromSite的意思是文章来自那个网站,如果文章都是原创的, 该字段就不会有值, 但是如果我们不传又会提示错误, 怎么办? 这时候就需要标记fromSite字段为非必填, 用"?"标记:
interface Article {
title: stirng;
count: number;
content:string;
fromSite?: string; // 非必填
}
// 不会报错
const article: Article = {
title: '为vue3学点typescript(2), 类型',
count: 9999,
content: 'xxx...',
}
用接口定义函数
接口不仅可以定义对象, 还可以定义函数:
// 声明接口
interface Core {
(n:number, s:string):[number,string]
}
// 声明函数遵循接口定义
const core:Core = (a,b)=>{
return [a,b];
}
类实现接口
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}