前言
在上一篇文章中,我们基本了解了Pinia的使用,本篇我们来手写实现Pinia的源码。
正文
前置知识
在实现Pinia源码时,需要对Vue3的组合式API有一定的了解:
ref
、 computed
、 watch
、reactive
、isRef
、isReactive
、toRefs
、provide
、inject
、effectScope
。
这里,我详细说明一下effectScope
:
EffectScope 就是类似一个组件的生命周期开始和结束,对应 EffectScope 的
run
和stop
当你在组件 setup 里使用响应式api时候,会进行依赖收集,也就是这些变量被这个组件使用了,产生的 computed 等 watcher 需要在组件销毁时也销毁。这些是自动的。
当你不在 setup 里,而在ES模块或什么地方直接使用响应式api时候,销毁这些 watcher 需要手动销毁。 为了方便,搞一个类似组件的依赖收集,也就是作用域包裹起来,直接销毁作用域,就可以销毁作用域执行期间创建的所有 watcher 了。
示例如下:
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
本文仅是实现pinia的功能,用的是anyscript。仅支持vue3。
createPinia、defineStore的基本实现
工具类: utils.ts
export function isObject(val: unknown): val is Record<string, any> {
return typeof val === 'object' && val !== null
}
export function isFunction(val: unknown) {
return typeof val === 'function'
}
export function isString(val: unknown): val is string {
return typeof val === 'string'
}
存放一些全局数据:rootStore.ts
export const piniaSymbol = Symbol()
// 全局的pinia实例
export let activePinia
// 设置全局的pinia实例
export const setActivePinia = pinia => (activePinia = pinia)
createPinia.ts
import { effectScope, ref } from 'vue'
import { piniaSymbol, setActivePinia } from './rootStore'
export function createPinia() {
const scope = effectScope()
// 存放每个store的state
const state = scope.run(() => ref({}))
const pinia = {
// 存放所有的store, 以store的id作为键值
_s: new Map(),
install(app) {
// 这里允许在组件外调用useStore
setActivePinia(pinia)
// 注入pinia实例,让所有store都可以访问到pinia
app.provide(piniaSymbol, pinia)
},
state
}
return pinia
}
store.ts
import { setActivePinia } from 'pinia'
import { computed, effectScope, getCurrentInstance, inject, reactive, toRefs } from 'vue'
import { activePinia, piniaSymbol } from './rootStore'
import { isString, isFunction } from './utils'
export function defineStore(idOrOptions, setup) {
let id
let options
if (isString(idOrOptions)) {
id = idOrOptions
options = setup
} else {
id = idOrOptions.id
options = idOrOptions
}
function useStore() {
const instance = getCurrentInstance()
let pinia: any = instance && inject(piniaSymbol)
if (pinia) {
setActivePinia(pinia)
}
// 这里activePinia肯定不为空,因为至少在安装pinia插件时已经设置过值了
pinia = activePinia!
if (!pinia._s.has(id)) {
// 第一次使用该store,则创建映射关系, Options Store
createOptionsStore(id, options, pinia)
}
const store = pinia?._s.get(id)
return store
}
return useStore
}
function createOptionsStore($id, options, pinia) {
const { state, getters, actions } = options
// store自己的scope,pinia._e是全局的scope
let scope
// 每个store都是一个响应式对象
const store = reactive<any>({})
// 对用户传入的state,getters,actions进行处理
function setup() {
// pinia.state是一个ref,给当前store的state赋值
pinia.state.value[$id] = state ? state() : {}
const localState = toRefs(pinia.state.value[$id])
// getters
const gettersArgs = Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = computed(() => {
return getters[name].call(store, store)
})
return computedGetters
}, {})
return Object.assign(localState, actions, gettersArgs)
}
const setupStore = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})
function wrapAction(name, action) {
return function () {
const args = Array.from(arguments)
// 确保this指向store
return action.apply(store, args)
}
}
for (let key in setupStore) {
const prop = setupStore[key]
if (isFunction(prop)) {
setupStore[key] = wrapAction(key, prop)
}
}
store.$id = $id
pinia._s.set($id, store)
Object.assign(store, setupStore)
return store
}
defineStore实现Setup Store
Setup Store 和Options Store的区别是SetupStore是用户直接传入的。我们修改如下:
store.ts
import { setActivePinia } from 'pinia'
import { computed, effectScope, getCurrentInstance, inject, isReactive, isRef, reactive,toRefs } from 'vue'
import { activePinia, piniaSymbol } from './rootStore'
import { isString, isFunction } from './utils'
function isComputed(v) {
return !!(isRef(v) && (v as any).effect)
}
export function defineStore(idOrOptions, setup) {
const isSetupStore = typeof setup === 'function'
let id
let options
if (isString(idOrOptions)) {
id = idOrOptions
options = setup
} else {
id = idOrOptions.id
options = idOrOptions
}
function useStore() {
const instance = getCurrentInstance()
let pinia: any = instance && inject(piniaSymbol)
if (pinia) {
setActivePinia(pinia)
}
// 这里activePinia肯定不为空,因为至少在安装pinia插件时已经设置过值了
pinia = activePinia!
if (!pinia._s.has(id)) {
// 第一次使用该store,则创建映射关系
if (isSetupStore) {
createSetupStore(id, options, pinia)
} else {
// Options Store
createOptionsStore(id, options, pinia)
}
}
const store = pinia?._s.get(id)
return store
}
return useStore
}
function createSetupStore($id, setup, pinia, isOptions = false) {
// store自己的scope,pinia._e是全局的scope
let scope
// 每个store都是一个响应式对象
const store = reactive<any>({})
// 对于setup api 没有初始化状态
const initalState = pinia.state.value[$id]
if (!initalState && !isOptions) {
pinia.state.value[$id] = {}
}
const setupStore = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})
function wrapAction(name, action) {
return function () {
const args = Array.from(arguments)
// 确保this指向store
return action.apply(store, args)
}
}
for (let key in setupStore) {
const prop = setupStore[key]
if (isFunction(prop)) {
setupStore[key] = wrapAction(key, prop)
}
// 处理setup store,把ref、reactive放入到state中
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 是ref或reactive,并且setup store
if (!isOptions) {
pinia.state.value[$id][key] = prop
}
}
}
store.$id = $id
pinia._s.set($id, store)
Object.assign(store, setupStore)
return store
}
function createOptionsStore($id, options, pinia) {
const { state, getters, actions } = options
// 对用户传入的state,getters,actions进行处理
function setup() {
// pinia.state是一个ref,给当前store的state赋值
pinia.state.value[$id] = state ? state() : {}
const localState = toRefs(pinia.state.value[$id])
// getters
const gettersArgs = Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = computed(() => {
let store = pinia._s.get($id)
return getters[name].call(store, store)
})
return computedGetters
}, {})
return Object.assign(localState, actions, gettersArgs)
}
return createSetupStore($id, setup, pinia, true)
}
store.$patch
store.ts
// 合并两个对象
function mergeReactiveObject(target, state) {
for (let key in state) {
let oldValue = target[key]
let newValue = state[key]
// 都是对象,需要递归合并
if (isObject(oldValue) && isObject(newValue)) {
target[key] = mergeReactiveObject(oldValue, newValue)
} else {
target[key] = newValue
}
}
}
function createSetupStore($id, setup, pinia, isOptions = false) {
// store自己的scope,pinia._e是全局的scope
let scope
// $patch,可能传入一个对象或函数
function $patch(partialStateOrMutation) {
if (isFunction(partialStateOrMutation)) {
partialStateOrMutation(pinia.state.value[$id])
} else {
// 用新的对象合并原来的状态
mergeReactiveObject(pinia.state.value[$id], partialStateOrMutation)
}
}
const partialStore = {
$patch
}
// 每个store都是一个响应式对象
const store = reactive<any>(partialStore)
// 对于setup api 没有初始化状态
const initalState = pinia.state.value[$id]
if (!initalState && !isOptions) {
pinia.state.value[$id] = {}
}
const setupStore = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})
function wrapAction(name, action) {
return function () {
const args = Array.from(arguments)
// 确保this指向store
return action.apply(store, args)
}
}
for (let key in setupStore) {
const prop = setupStore[key]
if (isFunction(prop)) {
setupStore[key] = wrapAction(key, prop)
}
// 处理setup store,把ref、reactive放入到state中
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 是ref或reactive,并且setup store
if (!isOptions) {
pinia.state.value[$id][key] = prop
}
}
}
store.$id = $id
pinia._s.set($id, store)
Object.assign(store, setupStore)
return store
}
store.$reset
注意:store.$reset
仅支持Options store
function createOptionsStore($id, options, pinia) {
const { state, getters, actions } = options
// 对用户传入的state,getters,actions进行处理
function setup() {
// pinia.state是一个ref,给当前store的state赋值
pinia.state.value[$id] = state ? state() : {}
const localState = toRefs(pinia.state.value[$id])
// getters
const gettersArgs = Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = computed(() => {
let store = pinia._s.get($id)
return getters[name].call(store, store)
})
return computedGetters
}, {})
return Object.assign(localState, actions, gettersArgs)
}
const store = createSetupStore($id, setup, pinia, true)
store.$reset = function () {
const newState = state ? state() : {}
store.$patch(state => {
Object.assign(state, newState)
})
}
return store
}
store.$subscribe
原理就是利用watch
$subscribe(callback, options = {}) {
scope.run(() => {
watch(
pinia.state.value[$id],
state => {
callback({ storeId: $id }, state)
},
options
)
})
}
store.$onAction
发布订阅模式
subscribe.ts
export function addSubscription(subscriptions, callback) {
subscriptions.push(callback)
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
}
}
return removeSubscription
}
export function triggerSubscriptions(subscriptions, ...args) {
subscriptions.slice().forEach(cb => cb(...args))
}
store.ts
function createSetupStore($id, setup, pinia, isOptions = false) {
// store自己的scope,pinia._e是全局的scope
let scope
// $patch,可能传入一个对象或函数
function $patch(partialStateOrMutation) {
// 函数
if (isFunction(partialStateOrMutation)) {
partialStateOrMutation(pinia.state.value[$id])
} else {
// 用新的对象合并原来的状态
mergeReactiveObject(pinia.state.value[$id], partialStateOrMutation)
}
}
let actionSubscriptions = []
const partialStore = {
$patch,
$subscribe(callback, options = {}) {
scope.run(() => {
watch(
pinia.state.value[$id],
state => {
callback({ storeId: $id }, state)
},
options
)
})
},
$onAction: addSubscription.bind(null, actionSubscriptions),
$dispose() {
// 清除响应式
scope.stop()
// 清除订阅
actionSubscriptions = []
// 删除store
pinia._s.delete($id)
}
}
// 每个store都是一个响应式对象
const store = reactive<any>(partialStore)
// 对于setup api 没有初始化状态
const initalState = pinia.state.value[$id]
if (!initalState && !isOptions) {
pinia.state.value[$id] = {}
}
const setupStore = pinia._e.run(() => {
scope = effectScope()
return scope.run(() => setup())
})
function wrapAction(name, action) {
return function () {
const afterCallbackList: any[] = []
const onErrorCallckList: any[] = []
function after(callbck) {
afterCallbackList.push(callbck)
}
function onError(callbck) {
onErrorCallckList.push(callbck)
}
triggerSubscriptions(actionSubscriptions, { after, onError })
const args = Array.from(arguments)
let ret
try {
// 确保this指向store
ret = action.apply(store, args)
} catch (e) {
triggerSubscriptions(onErrorCallckList, e)
}
if (ret instanceof Promise) {
return ret
.then(value => {
triggerSubscriptions(afterCallbackList, value)
return value
})
.catch(e => {
triggerSubscriptions(onErrorCallckList, e)
})
}
triggerSubscriptions(afterCallbackList, ret)
return ret
}
}
for (let key in setupStore) {
const prop = setupStore[key]
if (isFunction(prop)) {
setupStore[key] = wrapAction(key, prop)
}
// 处理setup store,把ref、reactive放入到state中
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 是ref或reactive,并且setup store
if (!isOptions) {
pinia.state.value[$id][key] = prop
}
}
}
store.$id = $id
pinia._s.set($id, store)
Object.assign(store, setupStore)
return store
}
store.$dispose
$dispose() {
// 清除响应式
scope.stop()
// 清除订阅
actionSubscriptions = []
// 删除store
pinia._s.delete($id)
}
store.$state
Object.defineProperty(store, '$state', {
get: () => pinia.state.value[$id],
set: state => {
$patch($state => Object.assign($state, state))
}
})
storeToRefs
将store解构时,会失去响应式,需要storeToRefs使得解构后任保持响应式
storeToRefs.ts
import { isReactive, isRef, toRaw, toRef } from 'vue'
export function storeToRefs(store) {
store = toRaw(store)
const refs = {}
for (let key in store) {
let value = store[key]
if (isRef(value) || isReactive(value)) {
refs[key] = toRef(store, key)
}
}
return refs
}
插件
createPinia.ts
import { App, effectScope, type Ref, ref } from 'vue'
import { Pinia, piniaSymbol } from './rootStore'
import { StateTree } from './types'
export function createPinia() {
const scope = effectScope()
const state = scope.run<Ref<Record<string, StateTree>>>(() => ref<Record<string, StateTree>>({}))!
const _p: any[] = []
const pinia: Pinia = {
use(callback) {
_p.push(callback)
// 可以链式调用
return this
},
// 存放所有store
_s: new Map(),
install(app: App) {
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
},
_e: scope,
_p,
state
}
return pinia
}
store.ts
的createSetupStore
方法
// 插件
pinia._p.forEach(extender => {
// 将插件的返回值作为store的属性
Object.assign(
store,
scope.run(() => extender({ store }))
)
})
使用插件
main.ts
import { createApp } from 'vue'
import { createPinia } from '@/pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
// 一个简单的持久化插件
pinia.use(({ store }) => {
let local = localStorage.getItem(store.$id + 'PINIA_STATE')
if (local) {
store.$state = JSON.parse(local)
}
store.$subscribe(({ storeId: id }, state) => {
localStorage.setItem(id + 'PINIA_STATE', JSON.stringify(state))
})
})
app.use(pinia)
app.mount('#app')