Vue3 学习笔记

Vue 3 核心技术

起步

为什么要学习 Vue 3

市场上, Vue 3 是一个必要的功能. 另外, Vue 3 已经成为了 Vue 的默认版本, Vue 2 已经成为了一个过去式, 不会有新的功能了. 不过 Vue 3 向前兼容, 大部分的内容都是可以直接使用的, 只需要一天的时间, 就可以上手 Vue 3.

Vue 3 有新的四个特性:

  1. 更容易进行维护. 组合式的 API, 并且更好的支持 TS.
  2. 更快的速度. 重写了 diff 算法, 模板编译优化, 同时更高效的组件初始化.
  3. 更小的体积. 按需引入即可.
  4. 更优化的数据响应式. Proxy 机制.

什么是组合式 API

之前我们写的叫做选项式 API, methods 都在 methods 中, data, watch, computed 都是一个一个的选项. 如果我们要实现一个功能, 代码就会被分散到各个地方, 不便于维护.

Vue 3 中, 就可以直接使用组合式 API, 将需要的东西写在一起, 直接调用方法就可以了. 功能 A 直接就是功能 A, 可以写在一起; 其他的功能也一样.

另外, Vue 3 中的组合式 API 写起来更加简单, 类似于 React 的函数式编程, 非常方便.

创建 Vue 3 项目

还是使用脚手架搭建一个新的项目: create-vue, 底层切换到了 Vite, 下一代的构建工具, 响应速度更快.

如果需要创建项目, 首先需要确认 NodeJS 版本: NodeJS >= 16. 随后, 直接在控制台执行命令即可创建:

1
npm init vue@latest

这里暂时只需要勾选 ESLint 进行代码检测, 别的都可以不选择. 随后进入目录并且安装依赖, 即可直接启动项目!

可以看到, 这个项目启动只需要 988 ms, 一秒都不到! 这就是 Vite 的好处. 打开页面, 看到效果后, 项目就此创建完成!

熟悉项目目录 & 文件

打开刚才创建好的项目目录, 其实和之前的样子差不太多.

|228

其中, vue.config.js 变成了 vite.config.js, 并且包管理器中的版本更高了, 直接查看一些关键文件吧, 主要发生变化的还是代码部分.


查看 main.js 中, 代码变成了这样子:

1
2
3
4
5
6
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

在 Vue 2 中, 我们是需要使用 new Vue() 来创建 Vue 实例的, 但是现在我们直接使用一个函数来实现功能. 这是对创建实例做了一个封装.

[!note] 封装
Vue 3 中, 很多东西都做了封装, 比如创建路由 createRouter, 创建仓库 createStore

这保证了多个实例的独立性

查看 App.vue 中, 还是三个部分. 并且除了样式, 后面的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>

<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />

<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>

<main>
<TheWelcome />
</main>
</template>

这里发现, script 上面多了一个 setup, 这代表可以直接编写组合式 API; 另外, template 中, 出现了多个根组件! 并且其中的组件也是不需要注册的, 直接使用就可以了, 和 react 很像.

index.html 中, 还是提供了一个挂载用的组件, 别的就没了.

1
2
3
4
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>

别的目录用法基本一样, components 还是存放组件.

组合式 API

介绍

组合式 API, 其实就是一系列的函数, 其中有各种的函数逻辑. 并且, 如果需要写组合式 API, 就必须要写 setup 作为标识.

setup 选项

执行时机

setup 其实也是一个选项. 在 Vue 2 的选项式中, 我们可以这样来写一个 setup, 它会在 beforeCreate 前面进行调用, 非常的早.

这里清空一下 App 中的代码, 删除其他的东西, 直接开始书写.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default {
setup() {
console.log('setup 调用')
},
beforeCreate() {
console.log('before Create调用')
}
}
</script>

<template>
Hello
</template>

查看控制台输出:

|203

可以看到, 执行的比 beforeCreate 都要早! 也因为没有实例, 所以压根没有 this 的指向. (其实这是好事, 因为不需要纠结 this 是什么了!)

创建 & 使用数据

我们可以在 setup 中创建数据或者函数, 但是需要 return 才能使用. 如果没有 return, 则会报错:

所以, 必须要 return, 可以理解为暴露出去才可以正常使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
export default {
setup() {
// 数据
const message = 'Hello Vue3!'
// 函数
const logMessage = () => {
console.log(message)
}
// 暴露出去
return {
message,
logMessage
}
}
}
</script>

<template>
<!--尝试直接使用setup中的数据-->
<div>message is {{ message }}</div>
<button @click="logMessage">调用函数</button>
</template>

至此, 页面效果输出正常!

|450

setup 语法糖

每次都需要 return 还是太复杂了, 所以为什么不简单一些呢? 其实在 Vue 3 中, 我们更加推荐直接在 script 标签后面加一个 setup, 一次性直接暴露其中的所有数据和函数.

1
2
3
4
5
6
7
8
<script setup>
// 数据
const message = 'Hello Vue3!'
// 函数
const logMessage = () => {
console.log(message)
}
</script>

现在, 页面上的数据和函数都可以正常访问, 简单多了!


其实 setup 语法糖的原理就是创建了一个 __sfc__ 对象, 这个对象中储存了所有的数据, 并且会自动 return 其中的内容, 进而简化写代码的量, 提升效率.

reactive 和 ref 函数

在 Vue 中, 默认的数据都不是响应式的. 只有通过 reactive 或者 ref 函数创建的数据才具有响应式的特征. 比如如下代码, 创建一个计数器, 以及对应的加减法按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
//定义Counter
let counter = 0
// 定义修改变量的方法
const setCounter = (newValue) => {
counter = newValue
console.log(`new Counter is ${counter}`)
}
</script>

<template>
<button @click="setCounter(counter - 1)">-</button>
<span>{{ counter }}</span>
<button @click="setCounter(counter + 1)">+</button>
</template>

尝试查看一下效果:

|300

可以发现, 数据发生了变化, 但是页面没有发生变化. 所以, 我们需要使用 reactive 或者 ref 函数来实现响应式. 对于 ref 来说, 直接传入值就好.

使用的时候:

  • 页面中直接写 counter 即可.
  • 代码中需要使用 counter.value 来获取, 或者操作其中的值.
  • 需要导入 ref 或者 reactive 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
// 引入ref
import {ref} from 'vue'

//通过ref定义Counter
let counter = ref(0)
// 定义修改变量的方法
const setCounter = (newValue) => {
counter.value = newValue
console.log(`new Counter is ${counter.value}`)
}
</script>

<template>
<button @click="setCounter(counter - 1)">-</button>
<span>{{ counter }}</span>
<button @click="setCounter(counter + 1)">+</button>
</template>

至此, 已经实现响应式了.

|82


对于 reactive 来说, 需要传入一个对象类型的数据, 而 ref 是通用的, 可以传入对象, 也可以传入简单类型. 使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
// 引入ref
import {reactive} from 'vue'

//通过ref定义Counter
const counter = reactive({
count: 0
})

// 定义修改变量的方法
const setCounter = (newValue) => {
counter.count = newValue
}
</script>

<template>
<button @click="setCounter(counter.count - 1)">-</button>
<span>{{ counter.count }}</span>
<button @click="setCounter(counter.count + 1)">+</button>
</template>

使用起来麻烦了一些, 需要套一个对象, 不过不需要 .value 来获取其中的值.

总结来说:

  1. reactive 和 ref 都可以生成响应式数据
  2. reactive 不能处理简单类型的数据, 必须是一个对象
  3. ref 支持更好, 但是必须通过 .value 访问修改
  4. ref 的内部基于 reative 实现
  5. 实际工作中, 推荐使用 ref

computed 函数

计算属性其实也应该和原来的属性在一起, 那么我们就可以使用 computed 函数来得到计算属性了. 计算属性的思想和 Vue 2 完全一致, 但是组合 API 下的写法变了.

  1. 导入 computed 函数
  2. 传入回调函数, 获取基于响应式的计算后的值

还是基于刚才的代码, 我希望获取两倍的数据, 就可以使用计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import {computed, ref} from 'vue'

const counter = ref(0)

const setCounter = (newValue) => {
counter.value = newValue
}

// 创建一个计算属性
const doubleCounter = computed(() => {
return counter.value * 2
})

</script>

<template>
<button @click="setCounter(counter - 1)">-</button>
<span>{{ counter }}</span>
<button @click="setCounter(counter + 1)">+</button>
<!--使用计算属性-->
<strong>两倍为: {{ doubleCounter }}</strong>
</template>

使用的时候还是一样的, 直接按照属性来写就好.

|221

[!info] 可写的计算属性, get 和 set 呢?

和 Vue 2 完全一致, 直接将里面改写一下, 传入一个对象即可.

1
2
3
4
5
6
7
8
9
10
11
// 可写的计算属性
const doubleCounter = computed({
get: () => counter.value * 2,
set: (newValue) => {
counter.value = newValue / 2
}
})

const handleBlur = (event) => {
doubleCounter.value = +event.target.value
}

现在这个计算属性就是可写的了.

|425

[!failure] 注意

  1. 计算属性中不应该出现副作用, 比如异步请求或者 DOM 操作
  2. 避免直接修改计算属性的值, 一般计算属性就是只读的

watch 函数

和 Vue 2 的作用完全一致, 可以侦听一个或者多个数据的变化, 和 computed 一样, 只是语法发生了变化.

多了两个额外的参数: immediate 和 deep, 代表立即执行和深度监视.

侦听单个数据

对于单个数据来说, 我们可以按照下面的这种来监听数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import {ref, watch} from "vue";

const counter = ref(0);

// 监听数据的变化, 并且添加一个执行的回调函数
watch(counter, (newValue, oldValue) => {
console.log(`old: ${oldValue}, new: ${newValue}`)
console.log('counter变化了哦!')
})
</script>

<template>
<h1>{{ counter }}</h1>
<button @click="counter--">-1</button>
<button @click="counter++">+1</button>
</template>

可以拿到新的值和老的值, 并且根据需要做需要做的事情. 这里面就适合做一些副作用的事情.

侦听多个数据

如果想要监听多个数据, 差不多, 不过第一个参数是一个数组, 第二个参数就是传入一个回调函数, 函数的每个参数都是一个数组, 代表新值和老值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import {ref, watch} from "vue";

const counter = ref(0);
const time = ref('1')

// 监听数据的变化, 并且添加一个执行的回调函数
watch([counter, time], ([newCounter, newTime], [oldCounter, oldTime]) => {
console.log('time或者counter变了!')
console.log(`${newCounter} -> ${oldCounter}`)
console.log(`${newTime} -> ${oldTime}`)
})
</script>

<template>
<h1>{{ counter }}</h1>
<button @click="counter--">-1</button>
<button @click="counter++">+1</button>
<button @click="time += '1'">Time + 1</button>
</template>

[!tip] 注意
第一个数组中存储的都是新的值, 第二个数组中存储的都是旧的值!

|188

额外配置项

对于函数来说, 我希望进入页面就执行, 或者需要深度监视, 都可以写在 watch 函数的第三个参数中, 作为一个对象进行传递即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import {ref, watch} from "vue";

const counter = ref(0);
watch(counter, (newValue) => {
console.log(`new value is ${newValue}`)
}, {
// 进入页面立刻执行
immediate: true,
// 深度监视
deep: true
})
</script>

<template>
<h1>{{ counter }}</h1>
<button @click="counter--">-1</button>
<button @click="counter++">+1</button>
</template>

现在, 已进入页面就会触发回调, 并且是深度侦听的.

|190

精确侦听某个属性

我现在有一个需求, 我希望一个对象的某个数据发生变化的时候, 我才调用侦听的函数, 应该怎么做呢? 这里需要用一个固定的语法: 第一个参数写: () => 对象.xxx.需要侦听的值

现在开始, 就实现了只侦听这一个数据了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup>
import {ref, watch} from "vue";

// 创建一个对象
const info = ref({
name: 'yyt',
age: 18
})

// 深度侦听某个属性
watch(() => info.value.age, (newVal) => {
// 这样就侦听成功了
console.log(`age变化了! 现在是 ${newVal}`)
})
</script>

<template>
<div>{{ JSON.stringify(info) }}</div>
<button @click="info.name = 'lc'">改name</button>
<button @click="info.age = 19">改age</button>
</template>

上面的代码, 只有改变 Age 才会调用侦听器.

|274

provide 和 inject 函数

这两个东西可以跨层级的, 向任意的底层组件传递数据和方法, 实现跨层级的组件通信. 相比 Vue 2, 这个东西使用起来更加方便一些.

比如我现在有 A->B->C 组件, 跨层级, 我的 A 想要拿到 C 的数据, 就可以使用跨层级的形式来传递了. 这个语法非常的简单:

1
2
3
4
5
// 在顶层组件中 我可以提供数据
provide('key', 顶层组件中的数据)

// 底层任意组件中, 都可以通过这个key来获取
const message = inject('key')

在顶层组件中, 可以准备数据, 并且进行 provide.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import ComA from "@/components/ComA.vue";
import {provide, ref} from "vue";

// 准备一个数据
const count = ref(0)

// 进行传递
provide('main-count', count)
</script>

<template>
<div>我是顶层组件, count is {{ count }}</div>
<button @click="count--">-</button>
<button @click="count++">+</button>
<hr/>
<ComA/>
</template>

随后, 子组件中, 都可以访问到了.

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import ComB from "@/components/ComB.vue";
// 可以直接接收数据
import {inject} from "vue";

const count = inject('main-count')
</script>

<template>
<div>我是A组件, count is {{ count }}</div>
<ComB/>
</template>

同时, 数据是同步的!

|325

那么, 我们尝试嵌套多层, 并且尝试修改一下:

1
2
3
4
5
6
7
8
9
10
<script setup>
import {inject} from "vue";

const count = inject('main-count')
</script>

<template>
<div>我是C组件, 在B的里面, count is {{ count }}</div>
<button @click="count++">C尝试修改count + 1</button>
</template>

查看效果如何:

|325

可以进行修改! 这就是 provide 和 inject 的强大之处.

生命周期 函数

写法有一些差异, 不过大差不差其实, 所有的对应的钩子函数前面都多了一个 on , 并且 createdbeforeCreated 被归类在了 setup 函数中. 比如进入页面就发送请求, 就可以放在 setup 中. 这里的意思就是直接写就好.

剩下的, 比如 mounted 周期函数, 就可以写在 onMounted 中进行调用. 只要调用对应的函数, 就是调用的生命周期函数. 调用语法如下:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import {onBeforeMount, onMounted} from "vue";

onMounted(() => {
console.log('Mounted生命周期')
})

onBeforeMount(() => {
console.log('beforeMounted调用')
})
</script>

输出如下, 就算顺序不对, 效果也是正确的!

|183

另外, 生命周期函数可以多次调用, 完全不会冲突, 而是按照顺序依次执行. 这样就可以把一个部分的东西写在一起, 方便维护代码.

组件间传参

父传子

基本的步骤如下:

  1. 父组件中给子组件绑定属性 (一样的)
  2. 子组件中, 通过 props 选项接收即可

这里的子组件, 我们通过另外一个函数: defineProps 来接收. 子组件中, 可以直接调用这个函数来定义传入的数据:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
// 接收父组件的传入的参数
defineProps({
message: String
})
</script>

<template>
<!--直接使用就好-->
<div>{{ message }}</div>
</template>

父组件中, 直接导入组件并且传参即可:

1
2
3
4
5
6
7
8
9
10
<script setup>
// 引入组件
import ComSon from "@/components/ComSon.vue";
</script>

<template>
<!--使用组件 并且传递参数-->
<ComSon message="Hello World"/>
<ComSon message="Good Lc And yyt"/>
</template>

|162

子传父

和 Vue 2 的思想是一样的:

  1. 父组件中给子组件通过 @ 绑定事件
  2. 子组件通过 emit 方法触发事件

这里的 emit 订阅还是通过一个编译器宏来实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import {ref} from "vue";

// 子组件中, 声明一下可以订阅的方法
const emit = defineEmits(['get-counter'])

// 定义一个数据
const counter = ref(0)

// 可以调用这个函数 将子组件的数据传递给父亲
const getCounter = () => {
emit('get-counter', counter.value)
}
</script>

<template>
<div style="display: flex; justify-content: space-evenly">
<button @click="counter--">-</button>
<div>{{ counter }}</div>
<button @click="counter++">+</button>
<button @click="getCounter">传递数据给父组件</button>
</div>
</template>

以上代码会在点击按钮后, 将数据传递给父组件.

父组件中, 可以绑定这个方法, 只要子组件发送了信号, 就可以进行数据的传递了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
// 引入组件
import ComSon from "@/components/ComSon.vue";
import {ref} from "vue";

// 定义变量 记录数据
const counter = ref(-1)

// 定义函数 用来从子组件获取数据
const setCounter = (newValue) => {
counter.value = +newValue
}
</script>

<template>
<!--使用组件 并且传递参数-->
<h3 style="text-align: center">
父组件: value = {{ counter }}
</h3>
<!--添加绑定数据-->
<ComSon @get-counter="setCounter"/>
</template>

这样就实现了一个手动的数据传递:

|425

案例

既然都做到这里了, 我们尝试实现一个类似数据双向绑定. 不过这里我写的比较复杂一些. 子组件中, 我们只需要监听这个数据的变化, 就发送一个 input 信号即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import {ref, watch} from "vue";

// 子组件中, 声明一下可以订阅的方法
const emit = defineEmits(['input'])

// 定义一个数据
const counter = ref(0)

// 数据变化的时候 就触发函数
watch(counter, (newValue) => {
emit('input', +newValue)
})
</script>

<template>
<div style="display: flex; justify-content: space-evenly">
<button @click="counter--">-</button>
<div>{{ counter }}</div>
<button @click="counter++">+</button>
</div>
</template>

随后, 父组件中监听 @input 事件, 进行数据的获取, 并且修改 counter 的值即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
// 引入组件
import ComSon from "@/components/ComSon.vue";
import {ref} from "vue";

// 定义变量 记录数据
const counter = ref(0)
</script>

<template>
<h3 style="text-align: center">
父组件: value = {{ counter }}
</h3>
<!--尝试双向绑定-->
<ComSon @input="args => {counter = args}"/>
</template>

至此, 简单的绑定效果就实现了!

|375

模板引用

获取 DOM

我们可以通过 ref 来获取页面中真实的 DOM 对象或者组件实例对象. 这样就可以快速的获取组件身上的属性和方法了. 比如登陆功能, 点击登陆的时候, 就可以直接获取输入框中的内容了.

这里还是使用 ref 对象, 不过一开始的 ref 需要传入一个 null 值. 随后, 给想要绑定的组件, 设置 ref 属性即可. 例如下面的登陆输入框案例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import {ref} from "vue";

// 创建空的ref对象
const userNameRef = ref(null);
const passwordRef = ref(null);

const handleClick = () => {
/*这里渲染后 才可以访问到其中的数据*/
/*通过.value 访问即可*/
console.log(userNameRef.value)
console.log(passwordRef.value)
}
</script>

<template>
<div>用户名 <input ref="userNameRef"></div>
<div>密码 <input ref="passwordRef"/></div>
<button @click="handleClick">登陆</button>
</template>

可以看到, 输出的东西就是 DOM 元素了, 那么自然, 我们就可以使用 .value 获取输入的值了.

|325

获取组件

我们可以直接绑定一个子组件, 快速的获取其中存放的 value! 比如这样子, 写一个 counter 子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import {ref} from "vue";

const counter = ref(0)
</script>

<template>
<div>
<button @click="counter--">-</button>
Counter -> {{ counter }}
<button @click="counter++">+</button>
</div>
</template>

接下来, 在父组件中, 尝试绑定这个组件并且输出数据看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import MyCounter from "@/components/MyCounter.vue";
import {ref} from "vue";

const counterRef = ref(null)

const getCounterInfo = () => {
console.log(counterRef.value)
}
</script>

<template>
<MyCounter ref="counterRef"/>
<button @click="getCounterInfo">获取组件内容</button>
</template>

输出发现, 对象上面没有我们想要的数据!

|320

这是因为, setup 的语法糖中, 组件的属性默认是没有开放的. 如果需要访问, 需要使用一个宏函数.

1
2
3
defineExpose({
// 需要暴露给父组件的数据
})

所以这里回到子组件, 进行设置:

1
2
3
4
5
6
7
8
9
10
<script setup>
import {ref} from "vue";

const counter = ref(0)

// 暴露数据
defineExpose({
counter
})
</script>

再次查看输出:

|400

还没有展开, 就可以看到有一个 counter 了. 这个就是我们导出的内容! 不妨尝试输出看看:

1
2
3
const getCounterInfo = () => {  
console.log(counterRef.value.counter)
}

|288

至此, 我们的属性已经成功获取了! 那么自然, 可以使用这个东西来实现数据的传递.

Vue 3.3 新特性

defineOptions

因为我们有的时候, 需要写很多的, 并列的嵌套的东西, 但是现在的组合式 API 是没法创建平级属性的, 有的时候就会很麻烦. 所以, 我们可以使用这个宏函数, 来实现添加和 setup 平级的属性.

1
2
3
4
5
6
7
<script setup>
// 我想要说明自己的名字是什么 可以配置额外选项
defineOptions({
name: 'ComCounter',
inheritAttrs: false
})
</script>

defineModel

我们之前如果想要实现双向绑定, 则需要写很多东西, 比如定义 props, 然后定义 emits, 这是非常繁琐没有必要的. 于是就有了这个新的特性, 可以快速的帮助我们实现数据的双向绑定!

父组件和子组件中, 都提供一个输入框用来演示. 子组件中, 可以这么写:

[!tip] 注意
这里的 :value 相当于绑定了一下数据, 另外 @input 实现了监听更改

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
const inputText = defineModel({type: String})
</script>

<template>
<div>子组件
<input
:value="inputText"
@input="e => inputText = e.target.value"
>
</div>
</template>

随后, 父组件中, 只需要说明绑定的是什么, 然后传入需要绑定的值就行了.

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import ComSon from "@/components/ComSon.vue";
import {ref} from "vue";

/*定义一个输入的值 进行绑定*/
const inputText = ref('')
</script>

<template>
<div>App组件 <input v-model="inputText"></div>
<ComSon v-model:model-value="inputText"/>
</template>

现在就实现了双向数据绑定了!

|325

Pinia

什么是 Pinia

Pinia 是 Vue 的最新状态管理工具, 是 Vuex 的替代品, 官方更加推荐我们使用 Pinia 进行状态管理. 这个状态管理的优势就是比 Vuex 更加简单, 并且只要有 Vuex 的基础, 上手 Pinia 就会非常快了.

Pinia 去掉了 modules 的概念, 每一个 store 都是一个独立的模块. 并且, 配合 TS 后, 使用起来回更加友好, 提供更加可靠的类型推断.

同时, Pinia 的 actions 可以直接操作 state, 逻辑更加清晰.

添加到项目

在一开始创建项目的时候就可以配置 Pinia, 也可以手动的添加到项目当中:

1
2
3
yarn add pinia
# 或者使用 npm
npm install pinia

随后, 我们只需要创建一个 Pinia 实例, 并且挂载后就可以使用了. 来到 main.js 中, 书写如下代码:

1
2
3
4
5
6
7
8
9
10
import {createApp} from 'vue'
import {createPinia} from 'pinia' // 导入Pinia
import App from './App.vue'

// 创建pinia实例
const pinia = createPinia()

const app = createApp(App)
app.use(pinia) // 挂载pinia
app.mount('#app')

这里拆分了一下 createApp, 不过其实是可以链式调用的.

基本语法

定义数据

这个是一个状态管理工具, 所以还是创建 store 文件夹. 另外考虑到一个文件就是一个仓库, 所以直接定义一个 counter.js 出来:

|157

我们直接使用组合式 API 的写法就行, Vue 2 的和 Vuex 使用一摸一样, 传递一个对象就好. 在组合式 API 中, 基本使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {defineStore} from "pinia";
import {ref} from "vue";

// 定义pinia 需要传入两个参数, 一个是仓库名称, 一个是箭头函数
export const useCounterStore = defineStore('counter', () => {
// 可以声明数据和操作数据的方法 方法就是action
// 另外也可以声明基于数据的计算属性, 也就是getters
const count = ref(0)

// 返回声明的数据即可
return {
count
}
})

访问数据

随后, 在需要使用数据的地方, 导入这个仓库并且使用.

1
2
3
4
5
6
7
8
9
<script setup>
import ComSon from "@/components/ComSon.vue";
// 导入仓库
import {useCounterStore} from "@/store/counter.js";
// 得到仓库实例
const counterStore = useCounterStore()
// 输出看看
console.log(counterStore)
</script>

输出后, 可以找到刚才的数据:

|425

那么我们就可以直接使用这两个数据了. (不要解构, 否则会失去响应式)

1
<div>父组件 counter = {{counterStore.count}}</div>

|187

成功地进行了访问! 在子组件中一摸一样, 直接使用即可.

1
2
3
4
5
6
7
8
9
<script setup>
import {useCounterStore} from "@/store/counter.js";

const counterStore = useCounterStore()
</script>

<template>
<div>子组件 counter == {{ counterStore.count }}</div>
</template>

数据访问成功!

|229

修改数据

当然, 直接提供操作方法就好, 一个函数就是一个 action, 修改 counter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {defineStore} from "pinia";
import {ref} from "vue";

// 定义pinia 需要传入两个参数, 一个是仓库名称, 一个是箭头函数
export const useCounterStore = defineStore('counter', () => {
// 可以声明数据和操作数据的方法 方法就是action
// 另外也可以声明基于数据的计算属性, 也就是getters
const count = ref(0)

// 定义修改数据的方法
const addCount = () => {
count.value++
}
const subCount = () => {
count.value--
}

// 返回声明的数据即可
return {
count,
addCount,
subCount
}
})

随后, 在需要使用的地方, 调用 store 身上的方法即可.

1
2
3
4
5
6
<template>
<div>父组件 counter = {{ counterStore.count }}</div>
<button @click="counterStore.subCount">-</button>
<button @click="counterStore.addCount">+</button>
<ComSon/>
</template>

至此, 数据已经可以正常的修改了, 并且所有使用的地方的数据都是响应式的!

|207

计算属性

我们都知道, 之前有一个 getters, 可以获取经过一定计算的数据, 这里就没那么麻烦了, 直接使用 Vue 3 的计算属性即可.

1
2
3
4
5
6
7
// 定义一个计算属性数据  
const doubleCount = computed(() => count.value * 2)

// 返回声明的数据即可
return {
doubleCount
}

随后, 就可以直接使用了, 一摸一样.

1
<div>counter * 2 = {{ counterStore.doubleCount }}</div>

|209

解构 store

storeToRefs

我们的 store 是不能随便的进行解构的. 一旦解构, 那么响应式就丢失了, 也就是修改后不会触发页面的修改. 如果希望解构后仍然保留响应式, 则可以使用这个方法来进行解构.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import ComSon from "@/components/ComSon.vue";
import {useCounterStore} from "@/store/counter.js";
import {storeToRefs} from "pinia";

const counterStore = useCounterStore()

// 如果需要进行解构 并且保留响应式 则需要使用方法
const {count, doubleCount} = storeToRefs(counterStore)
</script>

<template>
<div>父组件 counter = {{ count }}</div>
<div>counter * 2 = {{ doubleCount }}</div>
...
</template>

现在, 就可以直接使用解构后的数据, 并且保留响应式了.

方法解构

作为 actions, 其实是可以直接进行解构的, 不需要其他的方法来辅助. 至此, 完整的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import ComSon from "@/components/ComSon.vue";
import {useCounterStore} from "@/store/counter.js";
import {storeToRefs} from "pinia";

const counterStore = useCounterStore()

// 如果需要进行解构 并且保留响应式 则需要使用方法
const {count, doubleCount} = storeToRefs(counterStore)
// 方法直接解构即可
const {addCount, subCount} = counterStore
</script>

<template>
<div>父组件 counter = {{ count }}</div>
<div>counter * 2 = {{ doubleCount }}</div>
<button @click="subCount">-</button>
<button @click="addCount">+</button>
<ComSon/>
</template>

这样的代码就简单很多, 并且效果完善了!

数据持久化

基本配置

这里需要使用到一个插件: Pinia Plugin Persistedstate

首先, 安装一下这个包:

1
npm i pinia-plugin-persistedstate

随后, 在 main.js 中注册这个东西:

1
2
3
4
5
6
7
8
9
10
11
12
import {createApp} from 'vue'
import {createPinia} from 'pinia'
// 导入Pinia持久化插件
import persist from 'pinia-plugin-persistedstate'
import App from './App.vue'

// 创建pinia实例并使用持久化插件
const pinia = createPinia().use(persist)

const app = createApp(App)
app.use(pinia) // 挂载pinia
app.mount('#app')

使用

在需要持久化的模块中, 添加一个配置项即可. 比如上面的 Counter 模块, 可以在最后添加一个配置项:

1
2
3
4
5
6
7
8
9
import {defineStore} from "pinia";
import {computed, ref} from "vue";

export const useCounterStore = defineStore('counter', () => {
...;
}, {
// 确认开启持久化
persist: true
})

现在在页面中, 如果刷新页面, 就可以发现数据已经不会消失了!

|325

一行代码都没改, 就是加了一个插件, 已经实现数据的持久化了!