Vue3.2 + Vite + TS + Pinia + Volar学习文档

851 阅读7分钟

前提条件:

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

  • 基于ProxyReflect,可以原生监听数组,可以监听对象属性的添加和删除。
  • 不需要一次性遍历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 仅对对象类型有效(对象、数组和 MapSet 这样的集合类型),而对 stringnumber 和 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');
  }
}