一、 ???創(chuàng)建源碼分析環(huán)境
?? 項目地址:https://github.com/mk965/read-pinia文章來源地址http://www.zghlxwxcb.cn/news/detail-424229.html
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_1
1. 創(chuàng)建一個 vue 項目
npm init vue@latest
# or
yarn create vite
2. Pinia 源碼入口
??源碼地址: github.com/vuejs/pinia
??打包文件: rollup.config.js
??入口文件: packages/pinia/src/index.ts
3. 復(fù)制 Pinia 源碼 & 在 main.ts 中初始化 Pinia 插件
將 pinia/packages/pinia/src
目錄下的所有文件復(fù)制到我們之前生成項目的/src/pinia
中。
在 main.ts
中安裝初始化 Pinia:
4. 添加必要倉庫依賴
此時通過 yarn dev
啟動項目時,會報缺少依賴。
在 rollup.config.js
第122行,可以看到依賴分別有
-
vue-demi
:一個可以幫助我們開發(fā)在vue2、vue3上通用的 vue 庫的開發(fā)工具。 -
vue
:vue 項目。 -
@vue/composition-api
:vue 組合式 api 插件。
121 | const external = ['vue-demi', 'vue', '@vue/composition-api']
在我們的項目中安裝好這兩個庫(vue 在創(chuàng)建項目時已經(jīng)安裝了)。
yarn add vue-demi @vue/composition-api
5. 添加必要環(huán)境變量
此時通過 yarn dev
啟動項目時,會報飲用錯誤。
Uncaught ReferenceError: __DEV__ is not defined
at rootStore.ts:97:3
環(huán)境變量位于 rollup.config.js
第167行:
const replacements = {
__COMMIT__: `"${process.env.COMMIT}"`,
__VERSION__: `"${pkg.version}"`,
__DEV__:
(isBundlerESMBuild && !isRawESMBuild) || (isNodeBuild && !isProduction)
? // preserve to be handled by bundlers
`(process.env.NODE_ENV !== 'production')`
: // hard coded dev/prod builds
JSON.stringify(!isProduction),
// this is only used during tests
__TEST__:
(isBundlerESMBuild && !isRawESMBuild) || isNodeBuild
? `(process.env.NODE_ENV === 'test')`
: 'false',
__FEATURE_PROD_DEVTOOLS__: isBundlerESMBuild
? `(typeof __VUE_PROD_DEVTOOLS__ !== 'undefined' && __VUE_PROD_DEVTOOLS__)`
: 'false',
// If the build is expected to run directly in the browser (global / esm builds)
__BROWSER__: JSON.stringify(isRawESMBuild),
// is targeting bundlers?
__BUNDLER__: JSON.stringify(isBundlerESMBuild),
__GLOBAL__: JSON.stringify(isGlobalBuild),
// is targeting Node (SSR)?
__NODE_JS__: JSON.stringify(isNodeBuild),
}
我們在 vite.config.ts
中添加缺少的環(huán)境變量:
export default defineConfig({
plugins: [vue()],
define: {
__DEV__: true,
__TEST__: true
}
})
6. 環(huán)境測試
在 src/pinia/createPinia.ts
中輸出字符串,查看控制臺是否正常打印,如正常打印則源碼分析環(huán)境正常運行:
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_1
二、??源碼分析(1)——執(zhí)行流程
在 defineStore
、 createPinia
、 useStore
等關(guān)鍵函數(shù)內(nèi)打印日志來確定執(zhí)行順序:
可以確定執(zhí)行順序為:
defineStore() ? main.ts ? createPinia() ? useStore()
其中 defineStore()
是在引用階段被調(diào)用,并返回 useStore()
函數(shù),之后便開始 Vue 的流程。注冊插件等,最后在頁面內(nèi)調(diào)用 useStore()
,創(chuàng)建 Store 等步驟。
三、??源碼分析(2)——createPinia
使用 Pinia 前需要在 vue 中初始化一個注冊 Pinia,注冊的方法是使用 :
import { createPinia } from 'pinia';
app.use(createPinia());
顯然 createPinia
函數(shù)返回的是一個 vue插件 。通過插件的方式安裝到 vue 中。
1. 源碼目錄
在 src/pinia/index.ts
目錄中可以找到:
export { createPinia } from './createPinia'
顯然 createPinia 函數(shù)的源碼目錄為: src/pinia/createPinia.ts
。
2. createPinia
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_2
在函數(shù)的最開始,通過 effectScope
聲明了一個 ref
,并賦值給了state,我們將其 簡單理解為聲明了一個 ref 并賦值給 state 。
?? effectScope 創(chuàng)建一個 effect 作用域,可以捕獲其中所創(chuàng)建的響應(yīng)式副作用 (即計算屬性和偵聽器),這樣捕獲到的副作用可以一起處理。對于該 API 的使用細節(jié),請查閱對應(yīng)的 RFC。
import { Pinia, PiniaPlugin, setActivePinia, piniaSymbol } from './rootStore';
import { ref, App, markRaw, effectScope, isVue2, Ref } from 'vue-demi';
import { registerPiniaDevtools, devtoolsPlugin } from './devtools';
import { USE_DEVTOOLS } from './env';
import { StateTree, StoreGeneric } from './types';
/**
* 創(chuàng)建應(yīng)用程序要使用的Pinia實例
*/
export function createPinia(): Pinia {
console.log('?? createPinia run!');
/**
* effectScope:
* 創(chuàng)建一個 effect 作用域,可以捕獲其中所創(chuàng)建的響應(yīng)式副作用 (即計算屬性和偵聽器),這樣捕獲到的副作用可以一起處理。對于該 API 的使用細節(jié),請查閱對應(yīng)的 RFC。
*/
const scope = effectScope(true);
// NOTE: 在這里,我們可以檢查窗口對象的狀態(tài),并直接設(shè)置它
// 如果有類似Vue 3 SSR的東西
const state = scope.run<Ref<Record<string, StateTree>>>(() => ref<Record<string, StateTree>>({}))!;
// 所有需要安裝的插件
let _p: Pinia['_p'] = [];
// 在調(diào)用 app.use(pinia) 前需要安裝的插件
let toBeInstalled: PiniaPlugin[] = [];
// 使用 markRaw 包裹的 pinia 使其不會變?yōu)轫憫?yīng)式
const pinia: Pinia = markRaw({
// app.use 執(zhí)行的邏輯
install(app: App) {
// 設(shè)置當(dāng)前使用的 pinia 實例
setActivePinia(pinia);
// 如果是 vue2 ,全局注冊已經(jīng)在 PiniaVuePlugin 完成,所以這段邏輯將跳過
if (!isVue2) {
// app 實例
pinia._a = app;
// 通過 provide 傳遞 pinia 實例,提供給后續(xù)使用
app.provide(piniaSymbol, pinia);
// 設(shè)置全局屬性 $pinia
app.config.globalProperties.$pinia = pinia;
/* istanbul ignore else */
if (USE_DEVTOOLS) {
registerPiniaDevtools(app, pinia);
}
// 處理未執(zhí)行插件
toBeInstalled.forEach((plugin) => _p.push(plugin));
// 處理完插件后清空
toBeInstalled = [];
}
},
/**
* 為 Pinia 提供安裝插件的能力
* @param plugin
* @returns Pinia
*/
use(plugin) {
// 如果 use 階段初始化完成則暫存 toBeInstalled 中
if (!this._a && !isVue2) {
toBeInstalled.push(plugin);
} else {
_p.push(plugin);
}
return this;
},
_p, // 所有的 pinia 插件
// it's actually undefined here
// @ts-expect-error
_a: null, // vue 實例,在 install 階段設(shè)置
_e: scope, // pinia 的作用域?qū)ο?,每個 store 都有單獨的 scope
_s: new Map<string, StoreGeneric>(), // store 緩存,key 為 pinia 的 id,value 為 pinia 對外暴漏的數(shù)據(jù)
state, // pinia 所有的 state 的合集,key 為 pinia 的 id,value 為 store 下所有的 state
});
// pinia devtools rely on dev only features so they cannot be forced unless
// pinia開發(fā)工具依賴于僅用于開發(fā)的功能,因此除非
// the dev build of Vue is used. Avoid old browsers like IE11.
// 使用Vue的開發(fā)版本。避免使用像IE11這樣的舊瀏覽器。
if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin);
}
return pinia;
}
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_2
四、??源碼分析(3)——defineStore
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_3
1. 三種創(chuàng)建方式
defineStore
所在位置: src/pinia/store.ts
進入文件之后可以看到通過函數(shù)重載的方式提供給我們?nèi)N參數(shù)傳遞方式:
其中參數(shù)含義如下:
-
id
:store 的 id,必須唯一。 -
options
: 與 Vue 的選項式 API 類似,我們也可以傳入一個帶有id
、state
、actions
與getters
屬性的 Option 對象。 -
storeSetup
:以setup
的方式創(chuàng)建,與 Vue 的 setup 函數(shù) 相似。在 storeSetup 中:-
ref()
等同于state
。 -
computed()
等同于getters
。 -
function()
等同于actions
。
-
2. defineStore 的執(zhí)行邏輯
在 defineStore
函數(shù)中,并沒有很特別的邏輯,首先是對三種創(chuàng)建方式進行兼容,然后定義一個名為 useStore
的函數(shù),然后返回 useStore
。
useStore
具體做了什么下節(jié)分析。
export function defineStore(
// TODO: add proper types from above
idOrOptions: any,
setup?: any,
setupOptions?: any
): StoreDefinition {
let id: string
let options:
| DefineStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
| DefineSetupStoreOptions<
string,
StateTree,
_GettersTree<StateTree>,
_ActionsTree
>
// 此處對三種創(chuàng)建方式進行兼容處理
const isSetupStore = typeof setup === 'function'
if (typeof idOrOptions === 'string') {
id = idOrOptions
// the option store setup will contain the actual options in this case
options = isSetupStore ? setupOptions : setup
} else {
options = idOrOptions
id = idOrOptions.id
}
// useStore
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
// ...
}
useStore.$id = id;
// 將 useStore 函數(shù)返回出去,但不會立即調(diào)用,在組件內(nèi)使用 store 時才會調(diào)用。
// 所以在 defineStore 中只是做了些兼容邏輯,然后返回一個函數(shù),返回的這個函數(shù)真正調(diào)用時才會觸發(fā)更多邏輯。
return useStore;
}
雖然我們前面定義了一個 store,但在我們使用 <script setup>
調(diào)用 useStore()
(或者使用 setup()
函數(shù),像所有的組件那樣) 之前,store 實例是不會被創(chuàng)建的。
<script setup>
import { useMainStore1 } from '@/stores/counter'
// 可以在組件中的任意位置訪問 `store` 變量 ?
const store = useMainStore1();
</script>
4. useStore
在之前我們分析了 defineStore
方法調(diào)用的時候返回了 useStore
方法,接下來看一下此方法究竟干了些什么。
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
Log('useStore()');
// 獲取當(dāng)前 vue 實例
const currentInstance = getCurrentInstance();
pinia =
// 在 test 模式下,忽略提供的參數(shù),因為我們總是可以通過 getActivePinia() 獲取 pinia 實例
// 如果 是test模式 && activePinia不為空 && activePinia是test模式 則為空 否則 返回參數(shù)中的pinia
// 或者 如果獲取到了當(dāng)前實例 并且 存在piniaSymbol 返回 inject(piniaSymbol, null) 否則 返回空
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
// 這里的 inject(piniaSymbol) 是在 createPinia 的 install 中 app.provide(piniaSymbol, pinia);
(currentInstance && inject(piniaSymbol, null));
console.log('pinia 實例 ==>', pinia);
// 將當(dāng)前 pinia 實例設(shè)置為激活的 pinia
// 如果存在多個 pinia 實例,方便后續(xù)邏輯獲取當(dāng)前pinia實例
if (pinia) setActivePinia(pinia);
// 在 dev環(huán)境 并且 獲取不到當(dāng)前 pinia 實例,則說明未全局注冊,拋出錯誤
if (__DEV__ && !activePinia) {
throw new Error(`[??]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n` + `\tconst pinia = createPinia()\n` + `\tapp.use(pinia)\n` + `This will fail in production.`);
}
// 將激活的 pinia 實例賦值給 pinia 變量,確保 pinia === activePinia。防止 setActivePinia 出錯導(dǎo)致兩個變量不一致
pinia = activePinia!;
// 如果 pinia 的 store 緩存中沒有當(dāng)前的 id,則創(chuàng)建新的 store,
// 否則直接獲取緩存中 store。
if (!pinia._s.has(id)) {
// 創(chuàng)建 store 并將其注冊在 pinia._s 中
if (isSetupStore) {
// 組合式
createSetupStore(id, setup, options, pinia);
} else {
// 選項式
createOptionsStore(id, options as any, pinia);
}
/* istanbul ignore else */
if (__DEV__) {
// @ts-expect-error: not the right inferred type
useStore._pinia = pinia;
}
}
// 獲取 pinia 緩存中的 store
const store: StoreGeneric = pinia._s.get(id)!;
// 開發(fā)環(huán)境 并且 是熱更新
if (__DEV__ && hot) {
const hotId = '__hot:' + id;
const newStore = isSetupStore ? createSetupStore(hotId, setup, options, pinia, true) : createOptionsStore(hotId, assign({}, options) as any, pinia, true);
hot._hotUpdate(newStore);
// cleanup the state properties and the store from the cache
delete pinia.state.value[hotId];
pinia._s.delete(hotId);
}
// save stores in instances to access them devtools
if (
__DEV__ &&
IS_CLIENT &&
currentInstance &&
currentInstance.proxy &&
// avoid adding stores that are just built for hot module replacement
!hot
) {
const vm = currentInstance.proxy;
const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {});
cache[id] = store;
}
// StoreGeneric cannot be casted towards Store
return store as any;
}
從上邊代碼中,可以發(fā)現(xiàn)最關(guān)鍵的兩個函數(shù)是 createSetupStore
、 createOptionsStore
,分別是創(chuàng)建 組合式Store 和 選項式Store。里邊包含了創(chuàng)建 store 的關(guān)鍵邏輯,下面分別來看一下。
5. createSetupStore
createSetupStore
的作用是創(chuàng)建一個組合式的 store,之后的 createOptionsStore
其實也是把 option
轉(zhuǎn)化后調(diào)用 createSetupStore
來創(chuàng)建 store。createSetupStore
的源碼很長,我們分批研究。對于一些變量的定義等內(nèi)容在此省略,只關(guān)注最核心邏輯,詳細的注釋可以查看 Github 中的源碼。
(1) 參數(shù)
createSetupStore
總共接收了6個參數(shù):
- $id :當(dāng)前 Store 的 ID,
- setup defineStore 或者 createOptionsStore 傳入的 setup 函數(shù)
- options 配置選項,state、getter、actions 等
- pinia Pinia 實例
- hot 熱更新相關(guān)
- isOptionsStore 是否是 選項式 Store 創(chuàng)建
/**
* 創(chuàng)建組合式 Store
* @param $id Store ID
* @param setup defineStore 傳入的 setup 函數(shù)
* @param options 配置選項
* @param pinia Pinia 實例
* @param hot 熱更新相關(guān)
* @param isOptionsStore 是否是 選項式 Store 創(chuàng)建
* @returns 創(chuàng)建的 store
*/
function createSetupStore<Id extends string, SS extends Record<any, unknown>, S extends StateTree, G extends Record<string, _Method>, A extends _ActionsTree>($id: Id, setup: () => SS, options: DefineSetupStoreOptions<Id, S, G, A> | DefineStoreOptions<Id, S, G, A> = {}, pinia: Pinia, hot?: boolean, isOptionsStore?: boolean): Store<Id, S, G, A> {
// ...
return store;
}
(2) 創(chuàng)建 Store
此過程中,創(chuàng)建一個 setupStore
常量,創(chuàng)建了一個作用域并執(zhí)行了 setup
函數(shù),獲取到 setup
函數(shù)中返回的內(nèi)容,也就是我們定義的 state、getter、action 等內(nèi)容。
在此過程中,state 的內(nèi)容也會被存儲到 pinia.state
中。action 則會被 wrapAction
處理。
對每一項 action 進行處理,目的是為了支持 $onAction
方法,此方法會在執(zhí)行 action 時執(zhí)行回調(diào)函數(shù),回調(diào)函數(shù)可以接收三個參數(shù)分別是:被調(diào)用的 store、action 的名字、傳遞給 action 的參數(shù)。在 store 中還會有一些基礎(chǔ)操作的 API ,請看下節(jié)。
// 在當(dāng)前 pinia 實例的緩存中新建一個作用域,在作用域中執(zhí)行 setup 函數(shù)
// 執(zhí)行的結(jié)果為 store 。 example: { count: ObjectRefImpl, increment: Function () }
const setupStore = pinia._e.run(() => {
scope = effectScope();
return scope.run(() => setup());
})!;
// 覆蓋現(xiàn)有操作以支持 $onAction
for (const key in setupStore) {
const prop = setupStore[key];
// ((如果是 ref) 并且 (不是 computed)) 或者 (是 reactive)
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
// 如果是 optionsStore 方式創(chuàng)建,option 結(jié)構(gòu)已經(jīng)在 createOptionsStore 將其加入 pinia
if (!isOptionsStore) {
// 將 ref 轉(zhuǎn)移到 pinia state 以保持一切同步
if (isVue2) {
set(pinia.state.value[$id], key, prop);
} else {
pinia.state.value[$id][key] = prop;
}
}
// 否則,如果是函數(shù)類型,那么它就是一個 action
} else if (typeof prop === 'function') {
// 如果是重寫這個值,應(yīng)該避免使用 wrapAction 重復(fù)包裝
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop);
// 這是一個熱更新模塊替換 store,因為 hotUpdate 方法需要在正確的上下文中執(zhí)行它
if (isVue2) {
set(setupStore, key, actionValue);
} else {
setupStore[key] = actionValue;
}
// 將 actions 存儲到插件配置的 actions 數(shù)組,以便它們可以在插件中使用
optionsForPlugin.actions[key] = prop;
}
}
(3) 基礎(chǔ) API
在 Pinia 的 store 中存在很多基礎(chǔ) API,比如:獲取 store id $id
、增加 action 調(diào)用回調(diào) $onAction()
、重置 store $reset()
、變更 store $patch()
、訂閱 $subscribe()
、移除 store $dispose
、獲取所有 state $state
等。我們逐個分析。
基礎(chǔ)的 API 首先被儲存在 partialStore
中,然后創(chuàng)建一個 store
常量,并且把這些基礎(chǔ) API 和 store 的內(nèi)容都合并到 store
常量中。
/**
* 具有 state 和 功能 的基本 store,但不能直接使用。
*/
const partialStore = {
_p: pinia,
$id,
$onAction,
$patch,
$reset,
$subscribe,
$dispose,
} as _StoreWithState<Id, S, G, A>;
(4) Store 和 基礎(chǔ) API 合并
在 (2) 和 (3) 中我們創(chuàng)建了 store 的基本內(nèi)容和基礎(chǔ)的API,現(xiàn)在新建一個變量,并把它們合并到一塊:
/**
* 創(chuàng)建一個響應(yīng)式的 store 對象
* 將基礎(chǔ)函數(shù)合并到 store 中
*/
const store: Store<Id, S, G, A> = reactive(
__DEV__ || USE_DEVTOOLS
? assign(
{
_hmrPayload,
_customProperties: markRaw(new Set<string>()), // devtools custom properties
},
partialStore
// must be added later
// setupStore
)
: partialStore
) as unknown as Store<Id, S, G, A>;
assign(toRaw(store), setupStore)
現(xiàn)在,還缺少一個獲取所有 state 得屬性: $state
,我們使用 defineProperty
給 store
增加 $state
屬性 :
// 使用它而不是 computed with setter 可以在任何地方創(chuàng)建它,而無需將計算的生命周期鏈接到首次創(chuàng)建 store 的任何地方。
// 給 store 定義 $state 屬性,方便獲取全部的 state
Object.defineProperty(store, '$state', {
get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
set: (state) => {
/* istanbul ignore if */
if (__DEV__ && hot) {
throw new Error('cannot set hotState');
}
$patch(($state) => {
assign($state, state);
});
},
});
(5) 對于 Pinia 自定義插件的處理
在之前的 createPinia()
方法中,Pinia 實例上存在一個 use()
方法是對自定義插件的支持,在這里我們需要對安裝的插件進行處理,調(diào)用左右的插件函數(shù),并給函數(shù)傳入 store app piain options
四個參數(shù)。
// apply 全部插件
pinia._p.forEach((extender) => {
console.log("插件安裝:", extender);
// 如果使用開發(fā)工具
/* istanbul ignore else */
if (USE_DEVTOOLS) {
const extensions = scope.run(() =>
// 調(diào)用插件,并傳入?yún)?shù)
extender({
store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!;
Object.keys(extensions || {}).forEach((key) => store._customProperties.add(key));
assign(store, extensions);
} else {
// 這里將插件返回的屬性合并到 store 中
assign(
store,
scope.run(() =>
extender({
store,
app: pinia._a,
pinia,
options: optionsForPlugin,
})
)!
);
}
});
我們可以這樣給 Piain 安裝插件:
const pinia = createPinia();
// 給 pinia 安裝插件
pinia.use((prop) => {
Log('Pinia 插件使用');
console.log('插件獲取到的參數(shù):', prop);
return {
$aaa: (param: string) => {
console.log('這里是插件安裝到 Pinia 上的功能');
console.log('prop', prop);
console.log('param', param);
},
};
});
app.use(pinia);
使用插件:
store.$aaa('傳遞的參數(shù)');
6. createOptionsStore
createOptionsStore
的代碼量比較少,從下面的代碼可以發(fā)現(xiàn),基本的邏輯就是從 options
中獲取到 state
、 actions
、 getters
,定義一個 setup
函數(shù)并調(diào)用 createSetupStore
創(chuàng)建 Store,還要將 getters
轉(zhuǎn)換為 computed
。
/**
* 創(chuàng)建 選項式 store
* @param id Store ID
* @param options 配置選項
* @param pinia Pinia 實例
* @param hot 熱更新相關(guān)
* @returns 創(chuàng)建的 store
*/
function createOptionsStore<Id extends string, S extends StateTree, G extends _GettersTree<S>, A extends _ActionsTree>(id: Id, options: DefineStoreOptions<Id, S, G, A>, pinia: Pinia, hot?: boolean): Store<Id, S, G, A> {
Log('createOptionsStore()');
const { state, actions, getters } = options;
const initialState: StateTree | undefined = pinia.state.value[id];
let store: Store<Id, S, G, A>;
/**
* 自定義一個 setup 函數(shù)
* @returns store
*/
function setup() {
if (!initialState && (!__DEV__ || !hot)) {
/* istanbul ignore if */
if (isVue2) {
set(pinia.state.value, id, state ? state() : {});
} else {
pinia.state.value[id] = state ? state() : {};
}
}
// 避免在 pinia.state.value 中創(chuàng)建 state
const localState =
__DEV__ && hot
? // 使用 ref() 解包狀態(tài)中的引用
toRefs(ref(state ? state() : {}).value)
: toRefs(pinia.state.value[id]);
return assign(
localState,
actions,
Object.keys(getters || {}).reduce((computedGetters, name) => {
if (__DEV__ && name in localState) {
// getter 不能和 state 屬性同名
console.warn(`[??]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`);
}
// 把 getter 轉(zhuǎn)為 computed
computedGetters[name] = markRaw(
computed(() => {
setActivePinia(pinia);
// it was created just before
const store = pinia._s.get(id)!;
// allow cross using stores
/* istanbul ignore next */
if (isVue2 && !store._r) return;
// @ts-expect-error
// return getters![name].call(context, context)
// TODO: avoid reading the getter while assigning with a global variable
return getters![name].call(store, store);
})
);
return computedGetters;
}, {} as Record<string, ComputedRef>)
);
}
store = createSetupStore(id, setup, options, pinia, hot, true);
return store as any;
}
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_3
五、??源碼分析(4)—— store 的基礎(chǔ) API 實現(xiàn)
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_3
1. $id
這個沒啥好說的,就是 createSetupStore
參數(shù)中的 $id
。
2. $onAction
設(shè)置一個回調(diào),當(dāng)一個 action 即將被調(diào)用時,就會被調(diào)用。 回調(diào)接收一個對象, 其包含被調(diào)用 action 的所有相關(guān)信息:
-
store
: 被調(diào)用的 store -
name
: action 的名稱 -
args
: 傳遞給 action 的參數(shù)
除此之外,它會接收兩個函數(shù), 允許在 action 完成或失敗時執(zhí)行的回調(diào)。
它還會返回一個用來刪除回調(diào)的函數(shù)。 請注意,當(dāng)在組件內(nèi)調(diào)用 store.$onAction()
時,除非 detached
被設(shè)置為 true, 否則當(dāng)組件被卸載時,它將被自動清理掉。
在 Pinia 的源碼中,關(guān)于 $onAction
的代碼是這樣的:
const partialStore = {
// ...
$onAction: addSubscription.bind(null, actionSubscriptions),
// ...
}
可以發(fā)現(xiàn), $onAction
就是給 addSubscription
函數(shù)綁定了個 null
的 this
和一個參數(shù),再來看一下這個 addSubscription
是何方神圣:
export const noop = () => {};
/**
* 添加訂閱
* @param subscriptions 訂閱者數(shù)組
* @param callback 回調(diào)
* @param detached
* @param onCleanup 當(dāng)清楚訂閱時的回調(diào)
* @returns 清除訂閱的回調(diào)
*/
export function addSubscription<T extends _Method>(
subscriptions: T[],
callback: T,
detached?: boolean,
onCleanup: () => void = noop
) {
subscriptions.push(callback);
// 移除訂閱
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback);
// 如果存在這個訂閱,在訂閱數(shù)組中移除掉,并執(zhí)行回調(diào)
if (idx > -1) {
subscriptions.splice(idx, 1);
// 執(zhí)行移除訂閱回調(diào)
onCleanup();
}
};
// detached 為 true 時,在當(dāng)前作用于停止時,不會刪除此訂閱,為 false 時會移除此訂閱
// getCurrentScope 如果有的話,返回當(dāng)前活躍的 effect 作用域
if (!detached && getCurrentScope()) {
// onScopeDispose: 在當(dāng)前活躍的 effect 作用域上注冊一個處理回調(diào)函數(shù)。當(dāng)相關(guān)的 effect 作用域停止時會調(diào)用這個回調(diào)函數(shù)。
onScopeDispose(removeSubscription);
}
// 返回移除訂閱的函數(shù)
return removeSubscription;
}
3. $patch
將一個 state 補丁應(yīng)用于當(dāng)前狀態(tài)。允許傳遞嵌套值。
$patch
允許兩種參數(shù)傳遞方式,傳入一個函數(shù),或一個 state 的補丁。
/**
* $patch 函數(shù)傳遞方式
* @param stateMutation
* @example store.$patch((state) => state.count += 200);
*/
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void;
/**
* $patch 對象傳遞方式
* @param partialState
* @example store.$patch({ count: 100 });
*/
function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void;
function $patch(partialStateOrMutator: _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void)): void {
Log('$patch', partialStateOrMutator);
// 訂閱收集器,保存收集到的訂閱者
let subscriptionMutation: SubscriptionCallbackMutation<S>;
isListening = isSyncListening = false;
// 重置 debugger 事件,因為 patches 是同步的
/* istanbul ignore else */
if (__DEV__) {
debuggerEvents = [];
}
// 對兩種傳參方式進行兼容
// 如果參數(shù)是函數(shù)
if (typeof partialStateOrMutator === 'function') {
// 如果是函數(shù),直接調(diào)用,并把 state 傳過去
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>);
// 收集訂閱,分別保存類型、id、事件
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
};
} else {
// 如果傳來的是 object
// merge 參數(shù)對象到當(dāng)前 store 的 state
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator);
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
};
}
//
const myListenerId = (activeListener = Symbol());
nextTick().then(() => {
if (activeListener === myListenerId) {
isListening = true;
}
});
isSyncListening = true;
// 在上方邏輯中,我們將 isListening isSyncListening 重置為 false,不會觸發(fā) $subscribe 中的 callback,所以需要手動進行訂閱發(fā)布
triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S>);
}
其中 triggerSubscriptions
方法是發(fā)布者,執(zhí)行訂閱函數(shù)的回調(diào):
/**
* 觸發(fā)訂閱者回調(diào)
* @param subscriptions 訂閱數(shù)組
* @param args 傳給回調(diào)的參數(shù)
*/
export function triggerSubscriptions<T extends _Method>(subscriptions: T[], ...args: Parameters<T>) {
subscriptions.slice().forEach((callback) => {
callback(...args);
});
}
4. $reset
通過建立一個新的狀態(tài)對象,將 store 重設(shè)為初始狀態(tài)。
/**
* $reset
* 只有 選項式 構(gòu)建的才可以使用此方法,
* 因為 state: () => ({count: 1}) 是一個函數(shù),只要重新調(diào)用就可以獲取原始值,
* 而 組合式 構(gòu)建的話 state 以 ref() 的形式實現(xiàn),無法獲取原始值。
*/
const $reset = isOptionsStore
? function $reset(this: _StoreWithState<Id, S, G, A>) {
const { state } = options as DefineStoreOptions<Id, S, G, A>;
// 取出 options 中的 state 函數(shù)重新執(zhí)行,以獲取到原始 state
const newState = state ? state() : {};
// 使用 $patch 更新 state,并分發(fā)訂閱
this.$patch(($state) => {
assign($state, newState);
});
}
: /* istanbul ignore next */
__DEV__
? () => {
// 如果是組合式語法構(gòu)建的話,拋出錯誤,因為 ref() 不能獲取到原始值
throw new Error(`??: Store "${$id}" is built using the setup syntax and does not implement $reset().`);
}
: // noop 是個空函數(shù),生產(chǎn)環(huán)境不拋出錯誤
noop;
5. $subscribe
設(shè)置一個回調(diào),當(dāng)狀態(tài)發(fā)生變化時被調(diào)用。它會返回一個用來移除此回調(diào)的函數(shù)。 請注意,當(dāng)在組件內(nèi)調(diào)用 store.$subscribe()
時,除非 detached
被設(shè)置為 true, 否則當(dāng)組件被卸載時,它將被自動清理掉。
/**
* 當(dāng)狀態(tài)發(fā)生變化時被調(diào)用
* 它會返回一個用來移除此回調(diào)的函數(shù)
* @param callback 回調(diào)
* @param options 配置
* @returns 返回一個取消訂閱的函數(shù),調(diào)用次函數(shù)時訂閱就被取消了
*/
function $subscribe(callback, options = {}) {
Log('$subscribe', options);
// 取消訂閱函數(shù)
const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher());
// effectScope:創(chuàng)建一個 effect 作用域,可以補貨其中所創(chuàng)建的響應(yīng)式副作用 (即計算屬性和偵聽器),這里用于捕獲 watch,以便于銷毀store的時候統(tǒng)一處理。
const stopWatcher = scope.run(() =>
// 從這里可以看出 pinia 的訂閱響應(yīng)式主要是依賴 vue 的 watch
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
);
}
},
assign({}, $subscribeOptions, options)
)
)!;
return removeSubscription;
}
其中, addSubscription
函數(shù)可以查看2. $onAction 。
6. $dispose
停止 store 的相關(guān)作用域,并從 store 注冊表中刪除它。 插件可以覆蓋此方法來清理已添加的任何副作用函數(shù)。 例如, devtools 插件停止顯示來自 devtools 的已停止的 store。
/**
* $dispose
* 停止 store 的相關(guān)作用域,并從 store 注冊表中刪除它。
* 插件可以覆蓋此方法來清理已添加的任何副作用函數(shù)。 例如, devtools 插件停止顯示來自 devtools 的已停止的 store。
*/
function $dispose() {
scope.stop();
subscriptions = [];
actionSubscriptions = [];
pinia._s.delete($id);
}
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_3
六、??源碼分析(5)—— 輔助函數(shù)
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_4
Pinia 也提供了一組類似 Vuex 的 映射 state 的輔助函數(shù)。你可以用和之前一樣的方式來定義 Store。這里不做使用的介紹,用法請看官網(wǎng):https://pinia.vuejs.org/zh/introduction.html
??源碼目錄: src/pinia/mapHelpers.ts
1. mapActions
mapActions
有兩種傳參方式,兩種傳參方式第一個參數(shù)都是 defineStore
中返回的 useStore
函數(shù)。
- 傳入一個對象,key 為映射到
methods
中的名字,value 為 action 的名字。 - 傳入一個數(shù)組,item 為 action 的名字。
下面是 mapActions
的源碼,主要思想就是調(diào)用 useStore
方法得到 Store ,然后取出需要的 action 并返回。
/**
* 這個方法需要傳入 useStore 和一個對象,可以在導(dǎo)入過程中給 action 改名,對象 key 為 action 的新名字,value 為 action 的舊名字
* 通過生成一個傳遞到組件的 methods 字段的對象, 允許直接使用 store 的 action,而不需要使用組合式 API(setup())。 該對象的值是 action, 而鍵是產(chǎn)生的方法名稱。
*
* @example
* ```js
* export default {
* methods: {
* // other methods properties
* // useCounterStore has two actions named `increment` and `setCount`
* ...mapActions(useCounterStore, { moar: 'increment', setIt: 'setCount' })
* },
*
* created() {
* this.moar()
* this.setIt(2)
* }
* }
* ```
*
* @param useStore - defineStore 返回的 useStore
* @param keyMapper - 為 action 定義新名稱的對象
*/
export function mapActions<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof A>
>(
useStore: StoreDefinition<Id, S, G, A>,
keyMapper: KeyMapper
): _MapActionsObjectReturn<A, KeyMapper>
/**
* 這個方法需要傳入 useStore 和一個數(shù)組,數(shù)組內(nèi)容為需要導(dǎo)入的 action 名稱
* 通過生成一個傳遞到組件的 methods 字段的對象, 允許直接使用 store 的 action,而不需要使用組合式 API(setup())。 該對象的值是 action, 而鍵是產(chǎn)生的方法名稱。
*
* @example
* ```js
* export default {
* methods: {
* // other methods properties
* ...mapActions(useCounterStore, ['increment', 'setCount'])
* },
*
* created() {
* this.increment()
* this.setCount(2) // pass arguments as usual
* }
* }
* ```
*
* @param useStore - defineStore 返回的 useStore
* @param keys - 要映射的 action 名稱數(shù)組
*/
export function mapActions<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A
>(
useStore: StoreDefinition<Id, S, G, A>,
keys: Array<keyof A>
): _MapActionsReturn<A>
/**
* 通過生成一個傳遞到組件的 methods 字段的對象, 允許直接使用 store 的 action,而不需要使用組合式 API(setup())。 該對象的值是 action, 而鍵是產(chǎn)生的方法名稱。
*
* @param useStore - defineStore 返回的 useStore
* @param keysOrMapper - array or object
*/
export function mapActions<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof A>
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: Array<keyof A> | KeyMapper
): _MapActionsReturn<A> | _MapActionsObjectReturn<A, KeyMapper> {
return Array.isArray(keysOrMapper)
// 如果傳入的是數(shù)組,遍歷這個數(shù)組取出所有 action 名稱
? keysOrMapper.reduce((reduced, key) => {
// @ts-expect-error
reduced[key] = function (
// 如果組件的具體類型無法獲得,或者你并不關(guān)心組件的具體類型,那么可以使用 ComponentPublicInstance
this: ComponentPublicInstance,
...args: any[]
) {
return useStore(this.$pinia)[key](...args)
}
return reduced
}, {} as _MapActionsReturn<A>)
// 如果傳入的是對象,keysOrMapper[key] 值為 action 名稱
: Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
// key 為新 name
// @ts-expect-error
reduced[key] = function (
this: ComponentPublicInstance,
...args: any[]
) {
return useStore(this.$pinia)[keysOrMapper[key]](...args)
}
return reduced
}, {} as _MapActionsObjectReturn<A, KeyMapper>)
}
2. $mapStore
通過生成一個對象,傳遞到組件的 computed
字段 以允許在不使用組合式 API(setup())
的情況下使用 store。 它接受一個 store 定義的列表參數(shù)。
/**
* 通過生成一個對象,傳遞到組件的 computed 字段 以允許在不使用組合式 API(setup())的情況下使用 store。 它接受一個 store 定義的列表參數(shù)。
*
* @example
* ```js
* export default {
* computed: {
* // other computed properties
* ...mapStores(useUserStore, useCartStore)
* },
*
* created() {
* this.userStore // store with id "user"
* this.cartStore // store with id "cart"
* }
* }
* ```
*
* @param stores - 要映射到 object 的 stores 列表
*/
export function mapStores<Stores extends any[]>(
// 所有參數(shù)放入 stores 數(shù)組,所以 store 不需要在包裹一層數(shù)組
...stores: [...Stores]
): _Spread<Stores> {
// 直接將 store 通過參數(shù)傳遞即可,不需要放到數(shù)組中,如果放到了數(shù)組中就拋出警告
if (__DEV__ && Array.isArray(stores[0])) {
console.warn(
`[??]: Directly pass all stores to "mapStores()" without putting them in an array:\n` +
`Replace\n` +
`\tmapStores([useAuthStore, useCartStore])\n` +
`with\n` +
`\tmapStores(useAuthStore, useCartStore)\n` +
`This will fail in production if not fixed.`
)
stores = stores[0]
}
// 遍歷所有傳進來的 useStore 并執(zhí)行,然后 return 出去就得到了所有的 store
return stores.reduce((reduced, useStore) => {
// $id 是 defineStore 添加的
// @ts-expect-error: $id is added by defineStore
reduced[useStore.$id + mapStoreSuffix] = function (
this: ComponentPublicInstance
) {
return useStore(this.$pinia)
}
return reduced
}, {} as _Spread<Stores>)
}
3. $mapState
通過生成一個對象,并傳遞至組件的 computed
字段, 以允許在不使用組合式 API(setup()
)的情況下使用一個 store 的 state 和 getter。 該對象的值是 state 屬性/getter, 而鍵是生成的計算屬性名稱。 你也可以選擇傳遞一個自定義函數(shù),該函數(shù)將接收 store 作為其第一個參數(shù)。 注意,雖然它可以通過 this
訪問組件實例,但它沒有標注類型。
/**
* 通過生成一個對象,并傳遞至組件的 computed 字段, 以允許在不使用組合式 API(setup())的情況下使用一個 store 的 state 和 getter。 該對象的值是 state 屬性/getter, 而鍵是生成的計算屬性名稱。 你也可以選擇傳遞一個自定義函數(shù),該函數(shù)將接收 store 作為其第一個參數(shù)。 注意,雖然它可以通過 this 訪問組件實例,但它沒有標注類型。
*
* @example
* ```js
* export default {
* computed: {
* // other computed properties
* // useCounterStore has a state property named `count` and a getter `double`
* ...mapState(useCounterStore, {
* n: 'count',
* triple: store => store.n * 3,
* // note we can't use an arrow function if we want to use `this`
* custom(store) {
* return this.someComponentValue + store.n
* },
* doubleN: 'double'
* })
* },
*
* created() {
* this.n // 2
* this.doubleN // 4
* }
* }
* ```
*
* @param useStore - defineStore 中返回的 useStore
* @param keyMapper - state 的屬性名 或 getters 的對象
*/
export function mapState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<
string,
keyof S | keyof G | ((store: Store<Id, S, G, A>) => any)
>
>(
useStore: StoreDefinition<Id, S, G, A>,
keyMapper: KeyMapper
): _MapStateObjectReturn<Id, S, G, A, KeyMapper>
/**
* Allows using state and getters from one store without using the composition
* API (`setup()`) by generating an object to be spread in the `computed` field
* of a component.
*
* @example
* ```js
* export default {
* computed: {
* // other computed properties
* ...mapState(useCounterStore, ['count', 'double'])
* },
*
* created() {
* this.count // 2
* this.double // 4
* }
* }
* ```
*
* @param useStore - defineStore 中返回的 useStore
* @param keys - state 的屬性名 或 getters 的數(shù)組
*/
export function mapState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
Keys extends keyof S | keyof G
>(
useStore: StoreDefinition<Id, S, G, A>,
// key數(shù)組,內(nèi)容僅限于 State 和 Getter 的 key
keys: readonly Keys[]
): _MapStateReturn<S, G, Keys>
/**
* Allows using state and getters from one store without using the composition
* API (`setup()`) by generating an object to be spread in the `computed` field
* of a component.
*
* @param useStore - defineStore 中返回的 useStore
* @param keysOrMapper - array or object
*/
export function mapState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: any
): _MapStateReturn<S, G> | _MapStateObjectReturn<Id, S, G, A> {
// 此處邏輯和 mapAction 很像
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
reduced[key] = function (this: ComponentPublicInstance) {
// 和 mapAction 的區(qū)別:mapAction 取出的是經(jīng)過 wrapAction 的 action ,然后在這調(diào)用了一下
return useStore(this.$pinia)[key]
} as () => any
return reduced
}, {} as _MapStateReturn<S, G>)
: Object.keys(keysOrMapper).reduce((reduced, key: string) => {
// @ts-expect-error
reduced[key] = function (this: ComponentPublicInstance) {
const store = useStore(this.$pinia)
const storeKey = keysOrMapper[key]
// 由于某種原因,TS 無法將 storeKey 的類型推斷為函數(shù)
return typeof storeKey === 'function'
? (storeKey as (store: Store<Id, S, G, A>) => any).call(this, store)
: store[storeKey]
}
return reduced
}, {} as _MapStateObjectReturn<Id, S, G, A>)
}
4. $mapGetters
mapGetters
已廢棄,直接使用 mapState
即可。
/**
* Alias for `mapState()`. You should use `mapState()` instead.
* @deprecated use `mapState()` instead.
*/
export const mapGetters = mapState
5. $mapWritableState
在使用 $mapState
把 state 導(dǎo)入 computed
時,如果直接去修改 state 的值是不允許的。
$mapWritableState
********除了創(chuàng)建的計算屬性的 setter,其他與 mapState()
相同, 所以 state 可以被修改。 與 mapState()
不同的是,只有 state
屬性可以被添加。
/**
* 除了創(chuàng)建的計算屬性的 setter,其他與 mapState() 相同, 所以 state 可以被修改。 與 mapState() 不同的是,只有 state 屬性可以被添加。
*
* @param useStore - store to map from
* @param keyMapper - object of state properties
*/
export function mapWritableState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof S>
>(
useStore: StoreDefinition<Id, S, G, A>,
keyMapper: KeyMapper
): _MapWritableStateObjectReturn<S, KeyMapper>
/**
* Allows using state and getters from one store without using the composition
* API (`setup()`) by generating an object to be spread in the `computed` field
* of a component.
*
* @param useStore - store to map from
* @param keys - array of state properties
*/
export function mapWritableState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
Keys extends keyof S
>(
useStore: StoreDefinition<Id, S, G, A>,
keys: readonly Keys[]
): {
[K in Keys]: {
get: () => S[K]
set: (value: S[K]) => any
}
}
/**
* Allows using state and getters from one store without using the composition
* API (`setup()`) by generating an object to be spread in the `computed` field
* of a component.
*
* @param useStore - store to map from
* @param keysOrMapper - array or object
*/
export function mapWritableState<
Id extends string,
S extends StateTree,
G extends _GettersTree<S>,
A,
KeyMapper extends Record<string, keyof S>
>(
useStore: StoreDefinition<Id, S, G, A>,
keysOrMapper: Array<keyof S> | KeyMapper
): _MapWritableStateReturn<S> | _MapWritableStateObjectReturn<S, KeyMapper> {
// 也是對于數(shù)組和對象的分別處理
// 返回包含 get 和 set 函數(shù)的對象,交給 computed 處理
return Array.isArray(keysOrMapper)
? keysOrMapper.reduce((reduced, key) => {
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
return useStore(this.$pinia)[key]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[key] = value as any)
},
}
return reduced
}, {} as _MapWritableStateReturn<S>)
: Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
// @ts-ignore
reduced[key] = {
get(this: ComponentPublicInstance) {
return useStore(this.$pinia)[keysOrMapper[key]]
},
set(this: ComponentPublicInstance, value) {
// it's easier to type it here as any
return (useStore(this.$pinia)[keysOrMapper[key]] = value as any)
},
}
console.log(reduced)
return reduced
}, {} as _MapWritableStateObjectReturn<S, KeyMapper>)
}
??????? 本節(jié)代碼:https://github.com/mk965/read-pinia/tree/article_4
?結(jié)語
代碼雖然比較多,但核心邏輯還是借助 vue 的 ref 和 reactive 實現(xiàn)響應(yīng)式。把 state
處理為 ref
,把 getters
處理成 computed
,提供一些基礎(chǔ)方法,并使用單例模式返回一個實例。
完整注釋代碼:文章來源:http://www.zghlxwxcb.cn/news/detail-424229.html
?? 項目地址:https://github.com/mk965/read-pinia
到了這里,關(guān)于深入 Pinia:從代碼出發(fā)探索 Vue 狀態(tài)管理的奧秘的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!