Pinia源码

参考资料
pinia的使用(原链接)
Pinia支持compositionApi同时也兼容optionsApi(this指向)
Pinia可以采用多个store,store之间可以互相调用(扁平化),不用担心命名冲突问题

Pinia基本使用

组合式 与 选项式 写法

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
import {defineStore} from '../pinia/store'
import {ref, computed} from 'vue'
export const useCounterStore = defineStore('counter', ()=>{
const count = ref(0)

const double = computed(()=>{
return count.value * 2
})

const increment = (num)=>{
count.value += num
console.log(todosStore.todos)
}

return {
count,
double,
increment
}
})
// export const useCounterStore = defineStore('counter', {
// state: () => {
// return {count: 0}
// },
// getters: {
// double() {
// return this.count * 2
// }
// },
// actions: {
// increment(num) {
// this.count += num
// }
// }
// })

Pinia实现原理

pinia中的最近数据存储在state中
_s 记录了state的各个时态, 存放各个时期的state, 有利于重复使用

创建原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { PiniaSymbol } from "./rootStore"
import {ref} from 'vue'

export function createPinia () {

const state = ref({})
const pinia = {
install(app) {
app.config.globalProperties.$pinia = pinia //更新
app.provide(PiniaSymbol, pinia)
},
state,
_s:new Map(),
}
return pinia
}

存储原理

其中参数含义如下:

  • id:store 的 id,必须唯一。

  • options: 与 Vue 的选项式 API 类似,我们也可以传入一个带有 id、 stateactions与 getters属性的 Option 对象。

  • storeSetup:以 setup 的方式创建,与 Vue 的 setup 函数 相似。在 storeSetup 中:

    • ref() 等同于 state
    • computed() 等同于 getters
    • function() 等同于 actions

getCurrentInstance 是 Vue 3 Composition API 中的一个函数,用于获取当前组件实例的内部对象

在调用useStore时, 首先会在_s中寻找所求值是否存在, 如果存在则直接赋值, 不存在则再创建一个新的值

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

export function defineStore(idOrOptions, setup) {
let id
let options

//判断是组合式还是选项式
const isSetupStore = typeof setup === 'function'

if(typeof idOrOptions == 'string') {
id = idOrOptions
options = setup
} else { //不是字符串, 说明传入了对象
options = idOrOptions
id = idOrOptions.id
}

function useStore() {
const currentInstance = getCurrentInstance()
const pinia = currentInstance && inject(PiniaSymbol)

if (!pinia._s.has(id)) { //这个store时第一次使用

if(isSetupStore){
createSetupStore(id, setup, pinia)
//创建组合式store
} else {
createOptionStore(id, options, pinia)
//创建后的store只需要存到_s中即可
}
}
//如果已经有了store, 不用创建, 直接拿到store就行了
const store = pinia._s.get(id)

return store
}
return useStore
}

组合式

this指向
当传入的值中含有方法对象时, 直接处理会改变对象的this指向, 这时候需要进行特殊处理
保证传入的类的this指向不改变
通过wrapAction函数显示转换他们的this指向

将处理好的值与旧值进行更新 (Object.assign)
加入到_s中, 在二次调用时减少计算

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
//setupStore 用户已经提供了完整的setup方法了, 我们只需执行setup函数即可,
//通过这个返回值, 将其放在store上即可

function createSetupStore(id, setup, pinia) {
const store = reactive({})

//处理this指向
function wrapAction (action) {
return function () {
//将action中的this永远处理成store, 保证this指向正确
return action.call(store, ...arguments)
}
}

const setupStore = setup()
//拿到的setupStore可能没有处理this指向

//处理this
for (let prop in setupStore) {
const value = setupStore[prop]
if (typeof value === 'function') {
setupStore[prop] = wrapAction(value)
}
}
Object.assign(store, setupStore)//加入到队伍里面

pinia._s.set(id, store)
return store
}

选项式

相比与组合式, 选项式多了一个setup()函数

在此过程中,state 的内容也会被存储到 pinia.state 中。action 则会被 wrapAction 处理

每一项 action 进行处理,目的是为了支持 $onAction 方法,此方法会在执行 action 时执行回调函数,回调函数可以接收三个参数分别是:被调用的 storeaction 的名字传递给 action 的参数

getters处理时, 需将他的逻辑执行完后,得到的值存入store

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
function createOptionStore(id, options, pinia) {
const {state, actions, getters={}} = options //store里面的数据, 已定义

const store = reactive({}) //pinia就是创建了一个响应式对象而已
//需要将这个api转换成组合式
function setup() {
pinia.state.value[id] = state ? state() : {}

let localState = toRefs(pinia.state.value[id])

return Object.assign(
localState,
actions,
Object.keys(getters).reduce((computeds, getterKey)=>{
computeds[getterKey] = computed(()=>{
return getters[getterKey].call(store)
})
return computeds
}, {})
)
}
//减少代码的重复书写, 此时选项式store已经将数据处理, 剩下的逻辑和处理组合式一致
createSetupStore(id, setup, pinia)
return store
}

store中的基础api实现

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 const actionSubscriptions = [] //所有订阅的actions时间, 都应该放在这个数组中
const partialStore = {
$patch,
//订阅状态
$subscribe(callback) {
watch(pinia.state.value[id], state => {
callback({id}, state)
})
},

//订阅用户的action操作
$onAction: addSubscription.bind(null, actionSubscriptions), //订阅
}
//pinia就是创建了一个响应式对象而已
const store = reactive(partialStore)

patch

将一个 state 补丁应用于当前状态。允许传递嵌套值。

$patch 允许两种参数传递方式,传入一个函数,或一个 state 的补丁。

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
47
48
49
    //这里需要获取原来的所有状态

    function $patch(partialStateOrMutator) {

        //partialStateOrMutator部分状态

        if(typeof partialStateOrMutator !== 'function') {

            //当前store中的全部状态, pinia.state.value[i]

            merge(pinia.state.value[id], partialStateOrMutator)

        } else {

            partialStateOrMutator(pinia.state.value[id])

        }

    }
        function merge(target, partialState) {

        for (const key in partialState) {

            if(!partialState.hasOwnProperty(key)) continue

            //原始的值

            const targetValue = target[key]

            //后来的值

            const subPatch = partialState[key]

            if (isObject(targetValue) && isObject(subPatch) && !isRef(subPatch)) { //ref也是对象

                target[key] = merge(targetValue, subPatch)

            } else {

                //如果不需要合并直接用新的覆盖掉老的即可

                target[key] = subPatch
            }
        }
    }
     if(isSetupStore) {
        pinia.state.value[id] = {}
        //用于存放setupStore中的状态
    }

reset

功能: 通过建立一个新的状态对象,将 store 重设为初始状态。

重置方法只能在optionStore中书写
因为在setupStore中, 用户自己控制状态;

  • state: () => ({count: 1}) 是一个函数,只要重新调用就可以获取原始值,
  • 而 组合式 构建的话 state 以 ref() 的形式实现,无法获取原始值。
1
2
3
4
5
6
7
    store.$reset = function() { //这个方法只支持optionAPI, 因为setupAPI中, 用户自己控制状态

        const newState = state ? state() : {}

        this.$patch(newState)

    }

subscribe

设置一个回调,当状态发生变化时被调用。它会返回一个用来移除此回调的函数。 请注意,当在组件内调用 store.$subscribe() 时,除非 detached 被设置为 true, 否则当组件被卸载时,它将被自动清理掉。

在store中的实现

1
2
3
4
5
6
7
8
9
10
11
        //订阅状态

        $subscribe(callback) {

            watch(pinia.state.value[id], state => {

                callback({id}, state)

            })

        },

onAction

设置一个回调,当一个 action 即将被调用时,就会被调用。 回调接收一个对象, 其包含被调用 action 的所有相关信息:

  • store: 被调用的 store
  • name: action 的名称
  • args: 传递给 action 的参数

除此之外,它会接收两个函数, 允许在 action 完成或失败时执行的回调。

它还会返回一个用来删除回调的函数。 请注意,当在组件内调用 store.$onAction() 时,除非 detached 被设置为 true, 否则当组件被卸载时,它将被自动清理掉。

调用方法

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
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))

}

定义

1
2
3
4
5
6
7
8
9
const actionSubscriptions = [] //所有订阅的actions时间, 都应该放在这个数组中

const partialStore = {
...
        //订阅用户的action操作

        $onAction: addSubscription.bind(null, actionSubscriptions),

   }

实现

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

  //处理this指向

    function wrapAction (action) {

        return function () {

            const afterCallbacks = []

            const onErrorCallbacks = []

            const after = (callback) => {

                afterCallbacks.push(callback)

            }

            const onError = (callback) => {

                onErrorCallbacks.push(callback)

            }

            triggerSubscriptions(actionSubscriptions, {after, onError}) //让用户传递after和error

            //将action中的this永远处理成store, 保证this指向正确



            //回调的方式

            let result

            try {//正常action是一个回调的情况, 可以直接拿到返回值出发after回调

            result =  action.call(store, ...arguments)

            triggerSubscriptions(afterCallbacks, result)

            } catch (e){

                triggerSubscriptions(onErrorCallbacks, e)

            }

            //如果返回值是一个Promise, 针对场景做处理  

            if(result instanceof Promise) {

                return result.then(value => {

                    triggerSubscriptions(afterCallbacks, value)

                }).catch(error => {

                    triggerSubscriptions(onErrorCallbacks, error)

                })
            }  

            return result
        }
    }

pinia插件写法

main.js中

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
const pinia = createPinia()

function persitisPlugin() { //为了用户传递参数, 所以用函数

    return ({store, id}) => { //所有的store都会执行此方法

        let oldState = JSON.parse(localStorage.getItem(id) || '{}')

        store.$state = oldState

        // store.$patch(oldState) //将老状态进行替换

        store.$subscribe((mutation, state) => {

            localStorage.setItem(id, JSON.stringify(state))

        })
    }
}

pinia.use(persitisPlugin())

//use API可以去调用插件的install方法,将app注入进来

app.use(pinia) //使用pinia插件

createStore中

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
import { PiniaSymbol } from "./rootStore"

import {ref} from 'vue'

export function createPinia () {

    //pinia是管理多个state状态的

    const state = ref({})

    const _p = [] //插件

    const pinia = {

        install(app) {

            app.config.globalProperties.$pinia = pinia

            app.provide(PiniaSymbol, pinia)

        },

        use(plugin) {

            _p.push(plugin)

            return pinia
        },

        state,

        _s:new Map(),

        _p
    }
    return pinia
}

总结

仓库链接
这里附上整个源码