Pinia 介绍
Pinia 最初是在 2019 年 11 月左右重新设计使用 Composition API 。从那时起,最初的原则仍然相同,但 Pinia 对 Vue 2 和 Vue 3 都有效,并且不必须使用组合 API, 除了安装和 SSR 之外,两者的 API 都是相同。
为什么使用 Pinia
- dev-tools 支持
- 热模块替换
- 插件:使用插件扩展 Pinia 功能
- 为 JS 用户提供适当的 TypeScript 支持
- 服务器端渲染支持
与 Vuex 的比较
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,作者意识到 Pinia 已经实现了在 Vuex 5 中想要的大部分内容。 与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。
RFC
Vuex 通过 RFC 从社区收集反馈,但 Pinia 没有,开发者根据开发应用程序、阅读他人代码、为使用客户工作以及在 Discord 上解答问题的经验来验证测试想法。
与 Vuex 3.x/4.x 的比较
Vuex 3.x 对应 Vue 2 而 Vuex 4.x 对应 Vue 3
Pinia API 与 Vuex ≤ 4 有很大不同,如下:
- mutations 去除
- 不需要自定义复杂包装器来支持 TS
- 无需注入、导入函数、调用函数自动完成
- 无需动态添加 Store
- 不再有 modules 嵌套结构(扁平架构)
- 没有命名空间模块
Pinia 使用
如果您的应用使用 Vue 2,还需要安装组合 API:@vue/composition-api。
安装
- Vue3
import { createPinia } from 'pinia'
app.use(createPinia())
- Vue2
// Vue 2,还需要安装一个插件 PiniaVuePlugin
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
el: '#app',
// ...
pinia,
})
定义 Store
- optionsStore
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// 第一种
export const useCountStore = defineStore({
id: 'count',
// 推荐使用 完整类型推断的箭头函数
state: () => ({
counter: 0
}),
getters: {
doubleCount: (state) => state.counter * 2
},
actions: {
increment() {
this.counter++
}
}
})
// 第二种
export const useCountStore = defineStore('count', {
state: () => ({
counter: 0
}),
getters: {
doubleCount: (state) => state.counter * 2
},
actions: {
increment() {
this.counter++
}
}
})
- setupStore
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCountStore = defineStore('count', () => {
const counter = ref(0)
const doubleCount = computed(() => counter.value * 2);
function increment() {
counter.value++;
}
return { counter, doubleCount, increment };
})
使用 Store
<script setup>
import { useCountStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
// 模板中可直接使用
const countStore = useCountStore()
// 解构 state、getter、action
const { counter, doubleCount } = storeToRefs(countStore)
const { increment } = countStore
</script>
State
1.修改状态
store.counter++$patch()
// 第一种
// 缺点:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合
store.$patch({
items: [{}, {}, ...]
counter: store.counter + 1,
})
// 第二种
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.counter += 1
})
$state
store.$state = { counter: 666 }
store.$state.counter = 666
2.重置状态
const store = useStore()
// 只支持 optionsStore
store.$reset()
3.订阅状态
store.$subscribe((mutation, state) => {
// ...
})
Getters
1.访问其他 getter
与计算属性一样,可以组合多个 getter,通过 this 访问任何其他 getter。
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
}
}
})
2.getter 传参
Getters 只是幕后的 computed 属性,因此无法向它们传递任何参数。 但是,可以从 getter 返回一个函数来接收参数
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => (userId) => state.users.find((user) => user.id === userId)
}
})
3.访问其他 Store 的 getter
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
Actions
同步操作、异步操作均支持
1.访问其他 store
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
// ...
}),
actions: {
async fetchUserPreferences(preferences) {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
2.订阅 Actions
const unsubscribe = someStore.$onAction(
({
name, // action 的名字
store, // store 实例
args, // 调用这个 action 的参数
after, // 在这个 action 执行完毕之后,执行这个函数
onError, // 在这个 action 抛出异常的时候,执行这个函数
}) => {
// 记录开始的时间变量
const startTime = Date.now()
// 这将在 `store` 上的操作执行之前触发
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 如果 action 成功并且完全运行后,after 将触发。
// 它将等待任何返回的 promise
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)
})
// 如果 action 抛出或返回 Promise.reject ,onError 将触发
onError((error) => {
console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)
})
}
)
// 手动移除订阅
unsubscribe()
Plugins(略过)
核心源码(不含 API)分析
为什么读源码?考虑 Pinia 能否作为微应用统一状态管理,在测试过程中发现导出的 Store 模块被其他服务引入后不能触发响应式
流程图
部分核心源码
app.use(createPinia())
createPinia()返回 pinia 实例对象,实例中包含 install、use 方法。install 方法用于 app 注册 Pinia 插件,use 方法用来给 pinia 扩展插件
function createPinia() {
const scope = effectScope(true);
const state = scope.run(() => ref({}));
let _p = [];
// 调用 app.use(pinia) 前 添加的插件
let toBeInstalled = [];
// markRaw 将对象标记为不可被转为代理
const pinia = markRaw({
install(app) {
setActivePinia(pinia);
if (!isVue2) {
pinia._a = app;
app.provide(piniaSymbol, pinia); // 给组件注入 pinia 实例
app.config.globalProperties.$pinia = pinia; // 给组件注入 pinia 实例,兼容 options api 写法
toBeInstalled.forEach((plugin) => _p.push(plugin));
toBeInstalled = [];
}
},
use(plugin) {
// 未调用 app.use(pinia) 且为 Vue3
if (!this._a && !isVue2) {
toBeInstalled.push(plugin);
} else {
_p.push(plugin);
}
return this;
},
_p, // pinia实例
_a: null, // vue实例
_s: new Map(), // 存储store
state,
});
return pinia;
}
defineStore()
defineStore()用于定义 Store,并返回 useStore 方法,在 useStore 执行时会动态创建 store(如果是 setupStore 则调用 createSetupStore 创建 store,如果是 optionsStore 则调用 createOptionsStore 创建 store)
生成的 store 会存储到 pinia._s
function defineStore(idOrOptions, setup, setupOptions) {
let id;
let options;
const isSetupStore = typeof setup === 'function';
// 根据传参类型来获取 store id 及 配置项 options
if (typeof idOrOptions === 'string') {
id = idOrOptions;
options = isSetupStore ? setupOptions : setup;
} else {
options = idOrOptions;
id = idOrOptions.id;
}
function useStore(pinia, hot) {
// 获取当前组件实例
const currentInstance = getCurrentInstance();
pinia = (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(currentInstance && inject(piniaSymbol, null))
if (pinia) setActivePinia(pinia);
pinia = activePinia;
// 判断定义的 store 是否存在,如果不存在则创建
if (!pinia._s.has(id)) {
// 创建 store 并注册到 `pinia._s`
if (isSetupStore) {
// 创建 setup 语法类型 store
createSetupStore(id, setup, options, pinia);
} else {
// 创建 options 语法类型 store
createOptionsStore(id, options, pinia);
}
}
// 根据 id 获取到对应 store 并返回
const store = pinia._s.get(id);
return store;
}
useStore.$id = id;
return useStore;
}
createSetupStore()
定义 $patch、 $reset、 $dispose、 $subscribe 方法,使用 reactive 创建响应式 store,遍历 setupStore 进行如下等价转换:
- setupStore 中的 ref、reactive 定义的变量等价于 optionsStore 中的 state
- setupStore 中的 computed 属性等价于 optionsStore 中的 getters
- setupStore 中的导出函数等价于 optionsStore 中的 actions
function createSetupStore($id, setup, options = {}, pinia, hot, isOptionsStore) {
let scope;
const $subscribeOptions = {
deep: true,
};
// internal state
let isListening; // state 监听回调执行时机标记,防止频繁触发回调函数
let isSyncListening; // state 监听回调执行时机标记,防止频繁触发回调函数
let subscriptions = markRaw([]); // 订阅状态, callback 回调队列
let actionSubscriptions = markRaw([]); // 订阅Actions, callback 回调队列
const initialState = pinia.state.value[$id]; // 初始状态
let activeListener;
function $patch(partialStateOrMutator) {
let subscriptionMutation;
isListening = isSyncListening = false;
if (typeof partialStateOrMutator === 'function') {
partialStateOrMutator(pinia.state.value[$id]);
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents,
};
} else {
// $patch 后状态更新
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator);
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents,
};
}
const myListenerId = (activeListener = Symbol());
// 状态修改完触发回调函数
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true;
}
});
isSyncListening = true;
// 触发订阅回调函数
triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id]);
}
// $reset 不支持 setup 语法的 store
const $reset = __DEV__
? () => {
throw new Error(`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`);
}
: noop;
function $dispose() {
scope.stop();
subscriptions = [];
actionSubscriptions = [];
pinia._s.delete($id);
}
// 包装 action 方法,返回处理订阅操作的包装函数
function wrapAction(name, action) {
return function () {
// ...
};
}
// 基础(部分) store
const partialStore = {
_p: pinia,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher());
const stopWatcher = scope.run(() => watch(() => pinia.state.value[$id], (state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback({
storeId: $id,
type: MutationType.direct,
events: debuggerEvents,
}, state);
}
}, assign({}, $subscribeOptions, options)));
return removeSubscription;
},
$dispose,
};
// 使用 reactive 创建响应式 store
const store = reactive(__DEV__ || USE_DEVTOOLS
? assign({ _hmrPayload, _customProperties: markRaw(new Set()) }, partialStore)
: partialStore);
// 注册 store 到 `pinia._s`
pinia._s.set($id, store);
const setupStore = pinia._e.run(() => {
scope = effectScope();
return scope.run(() => setup());
});
// 遍历 setupStore 属性,对其进行转换
for (const key in setupStore) {
const prop = setupStore[key];
// prop 是 ref(但不是 computed)或者 reactive
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 热更新相关,暂时忽略
if (__DEV__ && hot) {
set(hotState.value, key, toRef(setupStore, key));
} else if (!isOptionsStore) {
// 给 `pinia.state.value[$id]` 赋值
if (isVue2) {
set(pinia.state.value[$id], key, prop);
} else {
pinia.state.value[$id][key] = prop;
}
}
// 如果 prop 是 actions
} else if (typeof prop === 'function') {
// 对 actions 进行重新包装并返回包装后的函数
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop);
if (isVue2) {
set(setupStore, key, actionValue);
} else {
setupStore[key] = actionValue;
}
} else if (__DEV__) {
// 如果 prop 是 computed
if (isComputed(prop)) {
if (IS_CLIENT) {
// 将计算属性 key 存储到 getters队列 中
const getters = setupStore._getters || (setupStore._getters = markRaw([]));
getters.push(key);
}
}
}
}
if (isVue2) {
Object.keys(setupStore).forEach((key) => {
set(store, key, setupStore[key]);
});
} else {
assign(store, setupStore);
assign(toRaw(store), setupStore);
}
// 拦截 store 中 $state 属性,便于通过 store.$state 直接修改状态
Object.defineProperty(store, '$state', {
get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
set: (state) => {
$patch(($state) => {
assign($state, state);
});
},
});
return store;
}
createOptionsStore()
根据 options 构造 setup 函数(保证其返回值和 setupStore 中 setup回调返回值相同),调用 createSetupStore 方法生成响应式的 store,重写 $reset 方法
function createOptionsStore(id, options, pinia, hot) {
const { state, actions, getters } = options;
const initialState = pinia.state.value[id];
let store;
// 传递到 createSetupStore 方法中执行,获取
function setup() {
// 如果 pinia.state.value[id] 不存在,则进行初始化
if (!initialState && (!__DEV__ || !hot)) {
if (isVue2) {
set(pinia.state.value, id, state ? state() : {});
} else {
pinia.state.value[id] = state ? state() : {};
}
}
// 将 pinia.state.value[id] 各属性值转为响应式对象
const localState = __DEV__ && hot
? toRefs(ref(state ? state() : {}).value)
: toRefs(pinia.state.value[id]);
// 处理 getters 并将处理后的 getters 和 actions 合并到 localState 中
return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = markRaw(computed(() => {
setActivePinia(pinia);
const store = pinia._s.get(id);
if (isVue2 && !store._r)
return;
return getters[name].call(store, store);
}));
return computedGetters;
}, {}));
}
// 调用 createSetupStore 创建 store
store = createSetupStore(id, setup, options, pinia, hot, true);
// 重写 $reset 方法
store.$reset = function $reset() {
const newState = state ? state() : {};
this.$patch(($state) => {
assign($state, newState);
});
};
return store;
}
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情