各位观众老爷们,晚上好!今天咱不聊风花雪月,就来唠唠 Pinia 的源码,扒一扒 Store
实例的诞生,以及它身上的 state
、getters
、actions
这些“零件”是怎么组装起来的,让它们变得如此听话、如此响应式的。
准备好了吗?咱们这就开车了!
一、 Store
的“前世今生”:从 defineStore
到 useStore
Pinia 的 Store
,要说它的出生,得从 defineStore
这个“造物主”说起。defineStore
就像一个工厂,你给它提供一些原材料(state
、getters
、actions
),它就能生产出一个特定的 Store
。
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
这里的 useCounterStore
就是我们最终使用的 Store
钩子函数。它不是 Store
的实例本身,而是一个函数,调用它才能真正创建一个 Store
实例。
这个 useCounterStore
函数内部做了什么呢?简单来说,它完成了以下几件事:
- 创建
Store
实例: 这是最核心的一步,也是我们今天要重点剖析的。 - 注入到
app
(如果存在): 如果在 Vue 应用中使用,它会将Store
实例注入到 Vue 的app
中,方便在组件中使用useStore
获取Store
实例。 - 返回
Store
实例: 将创建好的Store
实例返回给使用者。
二、 Store
实例的“炼成术”:揭秘 createPinia
和 defineStore
Pinia 的核心在于响应式状态管理,这离不开 Vue 提供的响应式 API。在 Store
实例的创建过程中,state
、getters
、actions
都被巧妙地利用了 Vue 的响应式系统进行处理。
首先,让我们看看 createPinia
函数的作用。createPinia
主要负责创建一个 Pinia 的根实例,它包含了一个 state
用于存储所有 Store 的状态,以及一些插件相关的逻辑。
// 简化版 createPinia
function createPinia() {
const scope = effectScope(true)
const state = scope.run(() => reactive({}))
const pinia = {
install(app) {
app.provide(PiniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
},
state,
scope
}
return pinia
}
createPinia
创建了一个 reactive
的 state
对象,用于存储所有 Store 的状态。PiniaSymbol
是一个 Symbol,用于在 Vue 应用中提供 Pinia 实例。scope
用于管理 effect,方便在销毁 Pinia 实例时停止所有 effect。
接下来,咱们深入 defineStore
的内部,看看它是如何把 state
、getters
、actions
这些“零件”组装成一个响应式的 Store
实例的。defineStore
的简化版代码如下:
import { reactive, computed, toRefs, effectScope, getCurrentInstance, inject, unref } from 'vue'
import { PiniaSymbol } from './rootStore'
function defineStore(id, options) {
return () => {
const pinia = inject(PiniaSymbol)
if (!pinia) {
throw new Error('调用 useStore 时,Pinia 尚未安装')
}
const existingStore = pinia.state.value[id] // 检查是否已经存在 Store
if (existingStore) {
return existingStore
}
const scope = effectScope()
const store = scope.run(() => {
const state = options.state ? reactive(options.state()) : {}
const getters = {}
for (const getterName in options.getters) {
getters[getterName] = computed(() => {
// @ts-ignore
return options.getters[getterName].call(store, state)
})
}
const actions = {}
for (const actionName in options.actions) {
actions[actionName] = options.actions[actionName].bind(store)
}
const store = {
$id: id,
$state: state,
...state,
...getters,
...actions,
$reset: () => {
const newState = options.state ? options.state() : {};
Object.assign(store.$state, newState);
},
$dispose: () => {
scope.stop()
delete pinia.state.value[id]
}
}
// 将 state 的属性变成 ref
Object.keys(state).forEach(key => {
Object.defineProperty(store, key, {
get: () => state[key],
set: (value) => { state[key] = value }
})
})
return store
})
pinia.state.value[id] = store
return store
}
}
让我们逐行解析这段代码:
inject(PiniaSymbol)
: 从 Vue 应用中注入 Pinia 实例。如果没有注入,说明 Pinia 还没有安装,直接报错。pinia.state.value[id]
: 检查是否已经存在同名的Store
。如果存在,直接返回已有的Store
实例,避免重复创建。注意这里使用了pinia.state.value
, 因为pinia.state
是一个Ref
对象。effectScope()
: 创建一个effectScope
,用于管理当前Store
的所有副作用。当Store
销毁时,可以方便地停止所有副作用。reactive(options.state())
: 将options.state()
返回的对象转换为响应式对象。这是state
能够响应式更新的关键。computed(() => options.getters[getterName].call(store, state))
: 为每个getter
创建一个计算属性。计算属性会自动追踪依赖,并在依赖发生变化时重新计算。注意这里使用了call
方法,将store
作为this
上下文传递给getter
函数。options.actions[actionName].bind(store)
: 将每个action
绑定到store
实例上。这样在action
中就可以通过this
访问store
的state
、getters
和其他actions
。store
对象: 创建一个store
对象,包含$id
、$state
、getters
、actions
和一些辅助方法。注意这里使用了对象展开运算符...
,将state
和getters
的属性直接添加到store
对象上,方便使用。$reset
: 提供一个$reset
方法,用于将state
重置为初始值。$dispose
: 提供一个$dispose
方法,用于销毁Store
实例,停止所有副作用,并从 Pinia 实例中移除该Store
。- 将 state 的属性变成 ref: 这一步非常关键,它将
state
的每个属性都通过Object.defineProperty
重新定义,使得可以直接通过store.count
的方式访问和修改state
中的属性,同时保持响应式。
三、 state
的响应式魔法:reactive
和 toRefs
state
的响应式是 Pinia 的基石。defineStore
使用 reactive
函数将 state
对象转换为响应式对象。这意味着,当 state
中的任何属性发生变化时,所有依赖该属性的组件都会自动更新。
const state = options.state ? reactive(options.state()) : {}
reactive
函数会将一个普通 JavaScript 对象转换为一个 Proxy 对象。Proxy 对象会拦截对该对象的所有操作,并在属性被访问或修改时触发相应的钩子函数。Vue 的响应式系统就是通过这些钩子函数来追踪依赖和触发更新。
除了 reactive
,toRefs
也是一个重要的工具。toRefs
可以将一个响应式对象的所有属性转换为 ref
对象。ref
对象是一个包含 value
属性的对象,访问或修改 value
属性会触发响应式更新。
import { toRefs } from 'vue'
const store = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
export default {
setup() {
const counterStore = store()
const { count, doubleCount } = toRefs(counterStore)
return {
count,
doubleCount,
increment: counterStore.increment,
}
},
template: `
<button @click="increment">{{ count }} - {{ doubleCount }}</button>
`,
}
在这个例子中,toRefs(counterStore)
会将 counterStore.count
和 counterStore.doubleCount
转换为 ref
对象。这样,在组件中就可以直接使用 count
和 doubleCount
,而不需要通过 counterStore.count
和 counterStore.doubleCount
访问。
四、 getters
的“计算之道”:computed
的妙用
getters
就像 Store
的计算属性,它们可以根据 state
的值计算出新的值。defineStore
使用 computed
函数为每个 getter
创建一个计算属性。
const getters = {}
for (const getterName in options.getters) {
getters[getterName] = computed(() => {
// @ts-ignore
return options.getters[getterName].call(store, state)
})
}
computed
函数会自动追踪 getter
函数中使用的 state
属性。当这些 state
属性发生变化时,computed
函数会自动重新计算 getter
的值。
getters
的一个重要特点是缓存。只有当 getter
函数中使用的 state
属性发生变化时,getter
的值才会被重新计算。否则,getter
会直接返回缓存的值。这可以有效地提高性能。
五、 actions
的“行为艺术”:this
的绑定和状态的修改
actions
是 Store
中定义的方法,用于修改 state
。defineStore
使用 bind
方法将每个 action
绑定到 store
实例上。
const actions = {}
for (const actionName in options.actions) {
actions[actionName] = options.actions[actionName].bind(store)
}
通过 bind
方法,action
函数中的 this
指向 store
实例。这样,在 action
函数中就可以通过 this
访问 store
的 state
、getters
和其他 actions
。
const store = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
},
incrementBy(amount) {
this.count += amount
},
reset() {
this.$reset()
}
},
})
在这个例子中,increment
、incrementBy
和 reset
都是 action
函数。在 increment
函数中,this
指向 store
实例,所以可以通过 this.count++
修改 state
中的 count
属性。在 reset
函数中,通过 this.$reset()
调用了 store 实例的 reset 方法。
六、 Store
的“生命周期”:$dispose
的作用
Store
实例也是有生命周期的,当它不再需要时,应该被销毁。defineStore
提供了一个 $dispose
方法,用于销毁 Store
实例。
const store = {
// ...
$dispose: () => {
scope.stop()
delete pinia.state.value[id]
}
}
$dispose
方法主要做了两件事:
scope.stop()
: 停止effectScope
中所有副作用。这可以防止内存泄漏。delete pinia.state.value[id]
: 从 Pinia 实例中移除该Store
。
虽然 Pinia 没有提供明确的 unmount
钩子,$dispose
方法可以在组件卸载时调用,以释放资源。
七、 总结:Store
的响应式之旅
咱们今天一起探索了 Pinia 源码中 Store
实例的创建和 state
、getters
、actions
的响应式绑定细节。简单回顾一下:
组件 | 作用 | 核心技术 |
---|---|---|
defineStore |
定义 Store,将 state、getters、actions 组装成一个响应式的 Store 实例 | reactive 、computed 、bind 、effectScope 、Object.defineProperty |
state |
存储 Store 的状态,通过 reactive 转换为响应式对象 |
reactive |
getters |
定义 Store 的计算属性,根据 state 的值计算出新的值,具有缓存功能 |
computed |
actions |
定义 Store 的方法,用于修改 state ,this 指向 store 实例 |
bind |
$dispose |
销毁 Store 实例,停止所有副作用,并从 Pinia 实例中移除该 Store | scope.stop() 、delete |
Pinia 的响应式核心在于 Vue 提供的响应式 API。通过巧妙地使用 reactive
、computed
和 bind
,Pinia 将 state
、getters
和 actions
紧密地联系在一起,构建了一个强大而灵活的状态管理系统。通过 Object.defineProperty
,实现了直接访问 state
属性的语法糖。
希望今天的讲座能帮助大家更深入地理解 Pinia 的源码,并在实际开发中更好地使用 Pinia。
下次再见!