Vue3状态管理

响应式API状态管理

在Vue3中,简单的场景可以用响应式API来做状态管理,使用reactive()创建一个响应式对象,并将它导入到多个组件中。

1
2
3
4
5
6
7
8
import { reactive } from 'vue'

export const store = reactive({
count: 0,
increment() {
this.count++
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From A: {{ store.count }}</template>

<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From B: {{ store.count }}</template>

当 store 对象被更改时, 都会自动更新它们的视图。

二、Pinia

虽然以上手动状态管理方案在简单的场景中已经足够了,但是在大规模的生产应用中海油以下几点需要考虑:

  • 更强的团队协作约定
  • 与Vue DevTools集成,包括时间轴、组件内部审查和时间旅行调试
  • 模块热更新
  • 服务端渲染支持

Pinia 就是一个实现了上述需求的状态管理库,由 Vue 核心团队维护,对 Vue 2 和 Vue 3 都可用。而且是官方推荐的。
相比于 Vuex,Pinia 提供了更简洁直接的 API,并提供了组合式风格的 API,最重要的是,在使用 TypeScript 时它提供了更完善的类型推导。
那么Pinia和Vuex的区别有哪些?

1、与 Vuex 3.x/4.x 的比较

Pinia API 与 Vuex ≤4 有很大不同,即:

  • mutations 不再存在。他们经常被认为是非常冗长。
  • 无需创建自定义复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断。
  • 不再需要注入、导入函数、调用函数、享受自动完成功能!
  • 无需动态添加 Store,默认情况下它们都是动态的,您甚至都不会注意到。请注意,您仍然可以随时手动使用 Store 进行注册,但因为它是自动的,您无需担心。
  • 不再有 modules 的嵌套结构。您仍然可以通过在另一个 Store 中导入和 使用 来隐式嵌套 Store,但 Pinia 通过设计提供平面结构,同时仍然支持 Store 之间的交叉组合方式。 您甚至可以拥有 Store 的循环依赖关系。
  • 没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,您可以说所有 Store 都是命名空间的。

2、Pinia使用

引入
1
2
3
yarn add pinia
// 或者使用 npm
npm install pinia
Vue3引入

创建一个 pinia(根存储)并将其传递给应用程序:

1
2
3
import { createPinia } from 'pinia'

app.use(createPinia())
Vue2引入

安装一个插件并将创建的 pinia 注入应用程序的根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
el: '#app',
// 其他选项...
// ...
// 注意同一个 `pinia` 实例可以在多个 Vue 应用程序中使用
// 同一个页面
pinia,
})
Store
定义Store

一个 Store (如 Pinia)是一个实体,它持有未绑定到您的组件树的状态和业务逻辑。换句话说,它托管全局状态。它有点像一个始终存在并且每个人都可以读取和写入的组件。它有三个概念,state、getters 和 actions 并且可以安全地假设这些概念等同于组件中的“数据”、“计算”和“方法”。

1
2
3
4
5
6
7
import { defineStore } from 'pinia'

// useStore 可以是 useUser、useCart 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useStore = defineStore('main', {
// other options...
})

这个 name,也称为 id,是必要的,Pinia 使用它来将 store 连接到 devtools,defineStore的第二个参数可接受两类值:Setup函数或Option对象。

OptionStore

与Vue的选项式API类似,我们也可以传入一个带有state、actions与getters属性的Option对象

1
2
3
4
5
6
7
8
9
10
11
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})

可以认为state是store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

Setup Store

与Vue组合式API的setup函数类似,我们也可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个对象包含暴露出去的属性和方法。

1
2
3
4
5
6
7
8
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}

return { count, increment }
})

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions
使用Store
1
2
3
4
5
6
<script setup lang="ts">
import { useStore } from '@/stores/counter'

// 您可以返回整个 store 实例以在模板中使用它
const store = useStore()
</script>

为了从 Store 中提取属性同时保持其响应式,您需要使用storeToRefs()。 它将为任何响应式属性创建 refs。

1
2
3
4
5
6
7
8
<script setup lang="ts">
import { storeToRefs } from 'pinia'
const store = useStore()
// `name` 和 `doubleCount` 是响应式引用
// 这也会为插件添加的属性创建引用
// 但跳过任何 action 或 非响应式(不是 ref/reactive)的属性
const { name, doubleCount } = storeToRefs(store)
</script>
State

state是store的核心部分,在pinia中,state定义为返回初始状态的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
// 所有这些属性都将自动推断其类型
counter: 0,
name: 'Eduardo',
isAdmin: true,
}
},
})

也可以定义一个接口定义State类型,并添加state()的返回值类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface UserInfo {
name: string
age: number
}

interface State {
userList: UserInfo[]
user: UserInfo | null
}

const useStore = defineStore('storeId', {
state: (): State => {
return {
userList: [],
user: null,
}
},
})
访问State

可以通过store实例访问状态来直接读取和写入状态。

1
2
3
const store = useStore()

store.counter++
重置State
1
2
3
const store = useStore()

store.$reset()
变更State

除了用store.count++直接改变state,也可以调用$patch方法,允许用一个state的补丁对象同时更改多个属性

1
2
3
4
5
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})

patch方法也接受一个函数来组合难以用补丁对象实现的变更

1
2
3
4
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
替换State

不能完全替换掉store的state,因为这样会破坏其响应性,但是可以patch它。

1
2
3
4
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })

也可以通过变更pinia实例的state来设置整个应用的初始state,常用于SSR中的激活过程。

1
pinia.state.value = {}
订阅State

类似于Vuex的subscribe方法,你可以通过store的subscribe()方法监听state及其变化,比起普通的watch(),使用subscribe的好处是subscriptions在patch后只触发一次。

1
2
3
4
5
6
7
8
9
10
artStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。

console.log(JSON.stringify(state))
})
Getter

Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数

1
2
3
4
5
6
7
8
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})

大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})

然后你可以直接访问 store 实例上的 getter 了

1
2
3
4
5
6
7
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
向getter传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数

1
2
3
4
5
6
7
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})

并在组件中使用:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// note you will have to use `getUserById.value` to access
// the function within the <script setup>
</script>

<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
Action

Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})

类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全)。不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),

actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// 让表单组件显示错误
return error
}
},
},
})
插件

由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是你可以扩展的内容:

  • 为 store 添加新的属性
  • 定义 store 时增加新的选项
  • 为 store 增加新的方法
  • 包装现有的方法
  • 改变甚至取消 action
  • 实现副作用,如本地存储
  • 仅应用插件于特定 store
    插件是通过 pinia.use() 添加到 pinia 实例的。最简单的例子是通过返回一个对象将一个静态属性添加到所有 store。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import { createPinia } from 'pinia'

    // 创建的每个 store 中都会添加一个名为 `secret` 的属性。
    // 在安装此插件后,插件可以保存在不同的文件中
    function SecretPiniaPlugin() {
    return { secret: 'the cake is a lie' }
    }

    const pinia = createPinia()
    // 将该插件交给 Pinia
    pinia.use(SecretPiniaPlugin)

    // 在另一个文件中
    const store = useStore()
    store.secret // 'the cake is a lie'
    这对添加全局对象很有用,如路由器、modal 或 toast 管理器。
简介

Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context

1
2
3
4
5
6
7
export function myPiniaPlugin(context) {
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
}

然后用 pinia.use() 将这个函数传给 pinia:

1
pinia.use(myPiniaPlugin)

插件只会应用于在 pinia 传递给应用后创建的 store,否则它们不会生效。

在插件中调用 $subscribe
1
2
3
4
5
6
7
8
pinia.use(({ store }) => {
store.$subscribe(() => {
// 响应 store 变化
})
store.$onAction(() => {
// 响应 store actions
})
})
添加新的选项

在定义 store 时,可以创建新的选项,以便在插件中使用它们。例如,你可以创建一个 debounce 选项,允许你让任何 action 实现防抖。

1
2
3
4
5
6
7
8
9
10
11
12
13
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},

// 这将在后面被一个插件读取
debounce: {
// 让 action searchContacts 防抖 300ms
searchContacts: 300,
},
})

然后,该插件可以读取该选项来包装 action,并替换原始 action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用任意防抖库
import debounce from 'lodash/debounce'

pinia.use(({ options, store }) => {
if (options.debounce) {
// 我们正在用新的 action 来覆盖这些 action
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})

注意,在使用 setup 语法时,自定义选项作为第 3 个参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
defineStore(
'search',
() => {
// ...
},
{
// 这将在后面被一个插件读取
debounce: {
// 让 action searchContacts 防抖 300ms
searchContacts: 300,
},
}
)

更多内容请查阅官方文档:https://pinia.vuejs.org/zh/cookbook/

请我喝杯咖啡吧~

支付宝
微信