为什么需要状态管理

通常在一个复杂的应用当中,不妨想象一下:
组件A是一个导航条,上面有一个部分展示了用户的用户名,组件B是用于用户详细资料展示的组件(通常是可以修改的).
如果此时用户修改了自己的用户名,那那意味着导航条上的用户名是不是也要跟着更改,并且其中会涉及异步操作。(
因为修改用户资料肯定是要先向服务端发送修改的请求,然后再根据请求的返回结果去同步数据) 这个时候客户端要怎么去更新所有组件中用到“用户”相关的数据。

那就硬着头皮写呗,一顿操作下来就会发现,在不同的组件里面写了很多很多类似的数据同步的代码。而且一旦项目大了起来,就容易逻辑紊乱,出现一些奇奇怪怪的bug

  • 数据流向无法掌控
  • 没有办法很好的调试

如何解决

这就可以扯到一个状态管理模式,让数据单向流动,也就是常说的
单项数据流

vuex的文档解释的非常清楚了。通过一个全局的应用的单例,并且遵循一定的规则,项目的代码结构将会更加清晰并且更易于维护。

vuex

vuex,大名鼎鼎的vuex。
这部分好像没有啥要记录的,vue2和vue3的写法不大一样而已。

实际上在简单的应用上没必要上vuex进行状态管理,vue3之后可以自己用provideinject
去设计一个轻量级的状态管理库。其实就是利用了vue3把响应式的一些核心api分离出来的特性。
通过readonly这个api去控制状态的单项流动特性。

pinia

pinia,一个轻量级的vue状态管理库。

一图阐明状态管理的好处

话虽如此,有些简单的应用,没什么复杂的业务逻辑也可以不使用pinia。

  • 官方推荐
    使用过vuex之后pinia的使用基本上可以无缝过度。支持vue2与vue3,支持两种编码风格配置式与组合式,最近关注了一下这俩的github仓库,发现vuex已经好久没有更新了
  • 轻量级
    最重要的是pinia更轻量级,它只有1kb。这方面狠狠的薄纱vuex
  • 使用typescript
    pinia是用typescript编写的。所以有更好的ts支持
  • 完全可扩展
    pinia给开发者提供了状态管理库的拓展方案:
    插件,而且npm上也有一定数量的第三方插件,比如 pinia-plugin-persistedstate

注意事项,与vuex不同的是,pinia可以直接访问仓库的状态,并且可以直接做修改,但是非常不推荐这样做。这样做了之后导致状态的更改全都散落在各个组件上,不好维护,对状态的读取与变更最好是通过getters和actions
而是推荐对于仓库状态的修改都要使用action来实现。并且在pinia里面是没有Mutation这个东西了,所有的同步与异步都使用actions去实现。

两种风格的的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import {defineStore} from 'pinia'

export const useCounterStore = defineStore('counter', {
state: () => ({count: 0, name: 'Eduardo'}),
persist: true,
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
setName(name) {
console.log(this)
this.name = name
},
// 异步增加
async asyncIncrement() {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
this.increment()
},
// 异步减少
async asyncDecrement() {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
this.decrement()
},
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import {defineStore} from "pinia";
import {computed, reactive} from "vue";

export const useListStore = defineStore('list', () => {
const todoList = reactive({
items: [{
text: "吃饭",
isCompleted: true
}, {
text: "睡觉",
isCompleted: false
}, {
text: "打豆豆",
isCompleted: false
},],
counter: 100
})
const doubleCount = computed(() => todoList.counter * 2)
const addItem = (newItem) => {
todoList.items.push({
text: newItem,
isCompleted: false
})
}
const setItemCompleted = (i) => {
todoList.items.map((item, index) => {
if (index === i) {
item.isCompleted = !item.isCompleted
}
})
}
const delItem = (i) => {
todoList.items.splice(i, 1);
}

return {
todoList,
addItem,
setItemCompleted,
delItem,
doubleCount
}
}, {
persist: true,
},
)

两种风格的写法都可以,但是更推荐使用组合式写法,因为pinia在源码中,实际上最后都会使用同一个函数去处理不同的写法,只是在处理配置式的写法上进行了一些逻辑判断,而组合式的写法不用进行这个处理

  • defineStore 函数内部的部分逻辑判断源码
1
2
3
4
5
6
7
8
9
10
11
12
13
        if (!pinia._s.has(id)) {
// creating the store registers it in `pinia._s`
if (isSetupStore) {
createSetupStore(id, setup, options, pinia); // 创建组合式风格的仓库
} else {
createOptionsStore(id, options, pinia); // 创建选项式风格的仓库
}
/* istanbul ignore else */
{
// @ts-expect-error: not the right inferred type
useStore._pinia = pinia;
}
}

createOptionsStore这个方法的源码当中,会进行一些逻辑处理,然后调用 createSetupStore

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function createOptionsStore(id, options, pinia, hot) {
const {state, actions, getters} = options;
const initialState = pinia.state.value[id];
let store;

function setup() {
if (!initialState && (!hot)) {
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value, id, state ? state() : {});
} else {
pinia.state.value[id] = state ? state() : {};
}
}
// avoid creating a state in pinia.state.value
const localState = hot
? // use ref() to unwrap refs inside state TODO: check if this is still necessary
toRefs(ref(state ? state() : {}).value)
: toRefs(pinia.state.value[id]);
return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => {
if (name in localState) {
console.warn(`[🍍]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`);
}
computedGetters[name] = markRaw(computed(() => {
setActivePinia(pinia);
// it was created just before
const store = pinia._s.get(id);
// allow cross using stores
/* istanbul ignore if */
if (isVue2 && !store._r)
return;
// @ts-expect-error
// return getters![name].call(context, context)
// TODO: avoid reading the getter while assigning with a global variable
return getters[name].call(store, store);
}));
return computedGetters;
}, {}));
}

store = createSetupStore(id, setup, options, pinia, hot, true);
return store;
}

可以看到最后还是调用了一个 createSetupStore 方法。

总结

  • 一定要很熟悉某个库或者api的使用,并且存在为什么要这样设计的疑惑、做一些相关的性能优化,再去阅读源码。不然会徒增心智负担。
  • 在阅读源码的时候不要纠结于某个具体的点,具体的某行代码,理解大致的逻辑即可。