声明:本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
21世纪的码农江湖里最重要的是什么? 规则
所谓规则,就是你必须要遵循一套书写规范,不能在代码里胡搞乱搞,请大家不要小看这一个小小的约束
鲁迅说过,没有规矩不成方圆
可在这个纷繁复杂的世界中,你一言我一语,看似开明,却办不成事, 要知道,谁都想自己的的东西,能够影响别人,被采纳,甚至被敬仰!!
你折腾jsx ,我就搞模板语法, 你发明函数式编程,我偏要面向对象!前端这个工种从兴起到现在,十几年来,各种人吵来吵去,掐架,撕逼,骂街!
其实说穿了,就一个目的,我做的或者我信仰的东西牛x
你是牛x 了,可我们痛苦了 vue
要学,react
要学,angular
也要学, 函数式编程,jsx ,依赖注入,hooks ,各种创新的东西层出不穷!
却苦了我们这帮 每天搬砖的工友,我们是在乎技术牛不牛逼吗?
不是的,我们只在乎砖头烫不烫手。
我们只想要有一套好用的,能快速解决问题的规则,让我们早点下班回家陪老婆孩子!
我们不想知道,到底什么是最长递增子序列,不想了解 diff 算法的核心原理,更不想痛苦的学习rxjs
这么复杂的概念!
我们的诉求就是快速的解决问题,我不想忙碌到半夜,状态憔悴,身心疲惫
那么到底怎样状态管理科学健康的能管理我们的状态?
很显然,pinia
这个优秀的状态管理工具(谐音梗),就能给你答案!
追根溯源
在 pinia
出现之前 ,vue官方的状态管理工具叫做vuex
他是为vue 量身定做的优秀的状态管理工具
其实之所以优秀,是因为比他更优秀的 pinia
还没出来,他只能顺理成章的成为首选
但是他有一个非常麻烦的缺点,规矩太多,用起来太麻烦!
我们是需要规则,可我们需要的是一个符合直觉,并且简单的规则
依稀记得
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
接下来让我们感受一下,vuex
的复杂程度!
我们知道,vue
是一个中庸的框架,他汲取了很多框架的营养 ,那么vuex
作为他的附属产品!
同样借鉴了借鉴了 Flux、Redux 和 The Elm Architecture。
通过单项数据流,对于跨组建的状态做统一的管理以及维护
当然,这个规则很好,就是
太麻烦了
例子如下:
首先创建store
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
cart: []
},
// 必须是同步的
mutations: {
addItemToCart(state, item) {
state.cart.push(item);
},
// 可以是同步也可以是异步
actions: {
addItemToCart({ commit }, item) {
// 假设我们需要调用API去检查库存
if (checkInventory(item)) {
commit('addItemToCart', item);
}
}
}
});
export default store;
在项目中使用
<template>
<div>
<ul>
<li v-for="item in cart" :key="item.id">{{ item.title }} - ${{ item.price }}</li>
</ul>
<button @click="addItemToCart(product)">Add to Cart</button>
</div>
</template>
<script>
export default {
computed: {
cart() {
return this.$store.state.cart;
}
}
methods: {
addItemToCart(product) {
this.$store.dispatch('addItemToCart', product);
}
}
};
</script>
以上例子中,我们可以发现,如果想改一个直,可以直接改同步的mutations
,如果要是异步
我们必须首先 dispatch
一个actions
,然后在actions
调用mutations
中的方法,在这个方法中再去改 state
依稀记得,我在最初学习的时候一连有两个想不通的问题?
- 1、mutations 为什么不能异步
- 2、mutations 为啥要多此一举难道就不能不要吗
1、mutations 为什么不能异步
这个问题在我当年作为一个刚进前端新手村的我来说,困惑了很久,困惑的点在于,他们都是函数,怎么能做到不能异步呢? 我为什么就不能改state
呢?
他到底有什么魔力呢?
直到,我看到了vuex作者的一个回答,醍醐灌顶
啥?,原来同步异步都行啊,他制定如此规则的原因竟然是 为了vue-devtool
,虽然这个理由有点牵强,可既然规则是别人制定的,那咱就得遵守,毕竟制定规则的那都是无与伦比的聪明人,虽然咱觉得不好用,但咱也提不出更好的办法不是
2、mutations 为啥要多此一举难道就不能不要吗?
在最开始的时候,我也很困惑,mutations
既然你只是为了给 vue-devtool
用的,那么怎么就不能想个办法直接用Action
给vue-devtool
不就行了吗,然后经过我仔细的研究发现
确实不行
因为每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而, Action 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
所以,虽然很麻烦,却不能不用
基于以上原因,人们都在苦vuex
久矣,甚至在vue3 发布的时候大家直接提出了,vuex5
的提案
其实官方团队也早就意识到这个问题,于是vuex 的职业生涯也走到了头,Pinia 横空出世,他几乎解决了vuex 所有痛点
Pinia 核心特性
-
Pinia 没有
Mutations
(最终要的) -
Actions
支持同步和异步 -
没有模块的嵌套结构
-
- Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系
-
更好的
TypeScript
支持 -
- 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型断
-
不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便
-
无需手动添加 store,它的模块默认情况下创建就自动注册的
-
Vue2 和 Vue3 都支持
-
- 除了初始化安装和SSR配置之外,两者使用上的API都是相同的
-
支持
Vue DevTools
-
- 跟踪 actions, mutations 的时间线
- 在使用了模块的组件中就可以观察到模块本身
- 支持 time-travel 更容易调试
- 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用
- 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能
-
模块热更新
-
- 无需重新加载页面就可以修改模块
- 热更新的时候会保持任何现有状态
-
支持使用插件扩展 Pinia 功能
-
支持服务端渲染
使用方式也非常简单
声明全局状态
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () => ({
name: '老骥farmer',
email: 'farme@example.com'
}),
actions: {
changeName(newName) {
this.name = newName
}
}
})
在项目中使用
<template>
<div>
<h1>{{ user.name }}</h1>
<button @click="user.changeName('老骥')">Change name</button>
</div>
</template>
<script>
import { useUserStore } from '@/store/user'
export default {
setup() {
const user = useUserStore()
return { user }
}
}
</script>
基于以上内容,我相信大家已经基本了解了pinia到底是什么?
但是其实这些东西,很多jym
也都讲过很多遍了,只是作为一篇文章,不讲似乎又不行。
但他却不是我们本次的重点是要看源码的
真真意义上的解剖pinia
,pinia到底是什么?
解剖pinia
所谓解剖pinia
其实我们只需要浅浅的搞明白4个核心问题:
- 1、他为什么比vuex 好用
- 2、 他的运行原理
- 3、 他是怎么省略Mutations
- 4、他的响应式是怎么实现的
好,接下来我们一个个解惑
pinia为什么比vuex 好用
在上方的解释中,我们介绍过 pinia
的特性,可老话说得好,光说不练假把式, 我们当然要来对比一下了
现在假设你有个需求, 有的jym
就说了,我没有
。。。。。 你有!!!
我现在有个全局name 并且,项目中的多处使用,此时,我们必须要引入全局状态管理工具了,
在vuex 中我们需要怎么做呢?
声明store
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
// 定义一个name,以供全局使用
name: '张三',
},
mutations: {
setName(state,name) {
state.name = name;
}
},
});
export default store;
获取store
<template>
<div>{{$store.state.name}}</div>
</template>
<script>
export default {
mounted() {
// 使用this.$store.state.XXX可以直接访问到仓库中的状态
console.log(this.$store.state.name);
},
};
</script>
修改store
<script>
export default {
mounted() {
this.$store.commit('setName', '李四');
console.log(`新值:${this.$store.state.name}`);
},
};
</script>
在pinia 我们需要怎么做呢?
声明store
//src/store/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
name: '张三'
}
}
})
获取store
<template>
<div>{{ userStore.name }}</div>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
</script>
甚至修改store 也可以很简单
userStore.name = '李四'
通过对比我们可以很直观的发现,pinia
有以下优点
- 1、 代码量,我就不解释了,高下立判, 如果包含异步,差距会更大
- 2、 代码清晰度,这个相信谁强谁弱,大家也是了然于心,毕竟总是通过this去找代码的关联,总是很难的,而我直接引入使用,你好歹改代码就能瞬间找到源码的位置
- 3、 易用性,当然也是
pinia
首屈一指,因为大家可以惊奇的发现,他非常符合vue 的代码习惯,我么基本没有学习成本,想用值,引入用就行,想改值,直接改就行,完全符合我们的直觉! - 4、pinia没有modules嵌套结构,是一个平面的结构,可创建不同的 Store
综上所述,你要非要用vuex
我也不拦着你!
pinia的运行原理
谈到pinia
的原理,其实我们都是知道的,并且,可能很多人还用过,因为他也是站在巨人的肩膀上
本质上就是利用了Vue 3提供的reactive
函数和watch
函数。当状态存储中的状态发生变化时,Pinia会自动更新依赖于该状态的组件。在组件中,可以使用computed
和watch
函数来监听状态存储中的状态,当状态发生变化时,组件会自动更新。
接下来我们简单的实现一个defineStore
方法来创建全局状态
既然要创建全局状态我们首先得写个defineStore
方法
import {
computed,
ComputedRef,
effectScope,
EffectScope,
inject,
markRaw,
reactive,
toRaw,
toRefs,
} from "vue";
import { getCurrentInstance } from "vue";
export const piniaSymbol = Symbol("pinia");
export function defineStore(options: {
id: string;
state: any;
getters: any;
actions: any;
}) {
let { id } = options;
// 实际运行函数
function useStore() {
const currentInstance = getCurrentInstance(); // 获取实例
let pinia: any;
if (currentInstance) {
pinia = inject(piniaSymbol); // 获取install阶段的pinia
}
if (!pinia) {
throw new Error("super-mini-pinia在mian中注册了吗?");
}
if (!pinia._s.has(id)) {
// 第一次会不存在,单例模式
createOptionsStore(id, options, pinia);
}
const store = pinia._s.get(id); // 获取当前store的全部数据
return store;
}
useStore.$id = id;
return useStore;
}
上述方法中,大家发现,其实本质上还是引用 vue 的composition API
这个库解决的问题,其实就是建立一种规范,以及编程范式
上述方法 其实就是创建一个hooks 然后hooks中返回各种容错处理之后的store
接下来就是怎么创建store
了
function createOptionsStore(id: string, options: any, pinia: any) {
const { state, actions, getters } = options;
function setup() {
pinia.state.value[id] = state ? state() : {}; // pinia.state是Ref
const localState = toRefs(pinia.state.value[id]);
return Object.assign(
localState, // 被ref处理后的state
actions, // store的action
Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = markRaw(
computed(() => {
const store = pinia._s.get(id)!;
return getters![name].call(store, store);
})
);
return computedGetters;
}, {} as Record<string, ComputedRef>) // 将getters处理为computed
);
}
let store = createSetupStore(id, setup, pinia);
//将 store 重设为初始状态。
store.$reset = function $reset() {
const newState = state ? state() : {};
this.$patch(($state: any) => {
Object.assign($state, newState);
});
};
return store;
}
上述方法,就是创建store
的核心方法,但是光创建store 是不够的,我们还有很多辅助函数,于是
/**
* 处理action以及配套API将其加入store
* @param $id
* @param setup
* @param pinia
*/
function createSetupStore($id: string, setup: any, pinia: any) {
// 将状态补丁应用于当前状态
function $patch(partialStateOrMutator: any): void {
// 简易版实现仅支持传入function
if (typeof partialStateOrMutator === "function") {
partialStateOrMutator(pinia.state.value[$id]);
}
}
// 停止store的所有effect,并且删除其注册信息
function $dispose() {
scope.stop(); // effect作用于停止
pinia._s.delete($id); // 删除effectMap结构
}
// 所有pinia的methods
let partialStore = {
_p: pinia,
$id,
$reset: () => {}, // 在createOptionsStore实现
$patch,
$dispose,
$onAction: () => console.log("onAction"), // 该版本不实现
$subscribe: () => console.log("subscribe"), // 该版本不实现
};
// 将effect数据存放如pinia._e、setupStore
let scope!: EffectScope;
const setupStore = pinia._e.run(() => {
scope = effectScope();
return scope.run(() => setup());
});
// 合并methods与store
// 这里实际返回的就是响应式后的内容,此时已经在全局和页面中的模板联系起来了
// 如此一来就能根据全局状态响应式的更改页面的内容
const store: any = reactive(
Object.assign(toRaw({}), partialStore, setupStore)
);
// 将其加入pinia
pinia._s.set($id, store);
return store;
}
ok齐活了, 上述方法,给pinia 这个实例中的所有内容配齐了并且全员响应式
其实,我们通过上述源码中,发现,pinia
的原理朴实无华,他难得地方,就一个——架构设计
你会发现,他一个创建的方法,要分为三层,并且各层各司其职!这才是我么应该学习的榜样!
pinia是怎么省略Mutations
我们在之前说过,在vuex中Mutations的必要性,是为了配合vue-devtools 。所以不能删除,必须按照规行事。那pinia
是怎么解决问题的呢?
经过我探究源码发现, 真的是只要思想不滑坡,方法总比困难多
我们先说一下为什么vuex
必须要有Mutations
本质原因很简单,不能监听,必须通过主动出发解决问题!
所以我们只能在同步的方法主动触发,在触发的时候同步给dev-tools
而pinia
做了什么事情呢?
用 watch,深层监听
源码如下 :
$subscribe(callback, options = {}) {
console.log("$subscribe");
// 注册修改响应监听
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
() => stopWatcher() // 执行stopWatcher实际上执行的是scope.run返回的watch,而执行watch的返回函数,也就是停止当前watch
);
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
// 如果等于sync会在修改后立即执行该watch,此时的isSyncListening为false 不会触发callback
// 如果不等于sync,修改后不会立刻触发watch
if (options.flush === "sync" ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
);
}
},
assign({}, $subscribeOptions, options) // watch的第三个参数 默认deep为true
// 如果希望副作用函数在组件更新前发生,可以将flush设为'post'(默认是'pre')
// 如果flush设置为'sync',一旦值改变,回调将被同步调用。
// 对于'pre'和'post',回调使用队列进行缓冲。回调只会被添加到队列中一次,即使被监视的值改变了多次。中间值将被跳过并且不会传递给回调。
// 默认值为'pre'
)
)!;
return removeSubscription;
},
$subscrib 方法,其实就是用来响应store
变化的
store.$subscribe(() => { // 响应 store 变化 })
那他跟dev-tools有什么关系呢?
很简单,我们调用store.$subscribe 传入dev-tools 相关api 不就行了吗 ,如此一来,只要sotre 变化,工具中回调就会被执行
代码如下:
store.$subscribe(
({ events, type }, state) => {
// 通知更新
api.notifyComponentUpdate()
api.sendInspectorState(INSPECTOR_ID)
if (!isTimelineActive) return
// rootStore.state[store.id] = state
const eventData: TimelineEvent = {
time: now(),
title: formatMutationType(type),
data: {
store: formatDisplay(store.$id),
...formatEventData(events),
},
groupId: activeAction,
}
// reset for the next mutation
activeAction = undefined
if (type === MutationType.patchFunction) {
eventData.subtitle = '⤵️'
} else if (type === MutationType.patchObject) {
eventData.subtitle = '🧩'
} else if (events && !Array.isArray(events)) {
eventData.subtitle = events.type
}
// 判断事件类型,改变工具中数据
if (events) {
eventData.data['rawEvent(s)'] = {
_custom: {
display: 'DebuggerEvent',
type: 'object',
tooltip: 'raw DebuggerEvent[]',
value: events,
},
}
}
api.addTimelineEvent({
layerId: MUTATIONS_LAYER_ID,
event: eventData,
})
},
{ detached: true, flush: 'sync' }
)
pinia的响应式是怎么实现的
pinia的响应式实现方式,我就不再赘述了,他的响应式其实就是vue的响应式,那么又有jym
问了,vue的响应式是怎么实现的
我。。。。
那么就请移步我写的另一个文章# Vue3 从ref 函数入手透彻理解响应式原理
到底啊要不要用全局状态管理
这个问题其实我要讨论的最重要的问题,因为,工具始终是工具,他就摆在那里,而决定你的项目好坏的是使用各种工具的人 于是,到底啊要不要用全局状态管理,就会理所当然的被摆上台面 ,也成了各大公司的热门面试题,
其实这个问题,我相信很多人都没有仔细的思考过,大多数可能会认为Pinia应该是标配
当我们回到事物的本质的,就会发现,得出的结论,可能与你的潜意识截然相反
Pinia 到底的是为了解决什么问题?
答案很简单,只是单纯为了解决各个组件中变量统一维护的问题
意识到这个问题,你就会突然发现,在大多数的情况下,我们根本用不到全局状态管理
举个例子,在我们大多数的项目中都是嵌套层级 ,也就是,一个大积木其中包含无数的小积木
此时我们用全局状态管理,反倒会增加项目的复杂度,我们只需要在顶层声明变量即可,通过vue
自身的api解决组件间传值的问题,例如: provide / inject
、 $attrs / $listeners
等等
那我们什么情况下需要用呢?
这个问题,我思考过很久,因为业界,总没有一个标准,也没有一个度,很多人的解决都很粗暴,不知道需不需要用,那就要用
然而随着我工作经验的慢慢积累,我发现,我们看待问题,其实还是要回到问题的本质
我们使用全局状态管理的目的是什么?
项目可维护,代码不乱!!!
这其实才是最朴实的诉求,因为所有的工具出现,都是为了解决问题
顺着这个原则,其实我相信大多数人都会立马豁然开朗。
就能很自然而然的判断到底需不需要Pinia
例如, 你的用户信息,登录token ,动态路由,等需要夸多组件使用的变量,这时候就需要放在统一的地方维护。自然而然的Pinia
就成了必须品,就能很好的保证项目可维护,代码不乱!!!
至于其他情况吗。你开心就好,毕竟千金难买我愿意!