WJ-Yuan / Notes

My Tech Notes
https://wj-yuan.github.io/Notes/
0 stars 0 forks source link

[Note] Pinia 源码阅读笔记 #10

Open WJ-Yuan opened 1 year ago

WJ-Yuan commented 1 year ago
  • version: 2.1.6
  • commitId: d120ec3adea97bcc7e2abd75385e45fc33e12af1
WJ-Yuan commented 1 year ago

createPinia

函数的功能是创建一个整个 app 的 pinia 实例,主要是理解 effectScope 和 markRaw 两个 vue3 API 的作用

export function createPinia(): Pinia {
  // 创建一个 effect 作用域,可以捕获其中的响应式副作用
  const scope = effectScope(true);
  // 转发 {} 的 state 值
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({}),
  )!;

  let _p: Pinia['_p'] = [];
  let toBeInstalled: PiniaPlugin[] = [];

  // 使其不具有响应
  const pinia: Pinia = markRaw({
    install(app: App) {
      // 将当前 pinia 设置为 activePinia
      setActivePinia(pinia);
      if (!isVue2) {
        pinia._a = app;
        // vue3 版本将 pinia 绑定到全局
        app.provide(piniaSymbol, pinia);
        app.config.globalProperties.$pinia = pinia;
        if (USE_DEVTOOLS) {
          registerPiniaDevtools(app, pinia);
        }
        toBeInstalled.forEach((plugin) => _p.push(plugin));
        toBeInstalled = [];
      }
    },

    use(plugin) {
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin);
      } else {
        _p.push(plugin);
      }
      return this;
    },

    _p,
    _a: null,
    _e: scope,
    _s: new Map<string, StoreGeneric>(),
    state,
  });

  if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin);
  }

  // 返回 Pinia 值
  return pinia;
}

主体功能代码如上,未注释的是一些关于插件的处理逻辑,暂时不关注这部分。总结下其实总体的思路就是:

  1. 创建一个响应式的作用对象
  2. 将这个对象(跨组件)提供给整个应用
WJ-Yuan commented 1 year ago

defineStore

我们都知道 Pinia 有两种形式来定义 store,因此在这个函数里

export function defineStore<Id extends string, SS>(
  id: Id,
  storeSetup: () => SS,
  options?: DefineSetupStoreOptions<
    Id,
    _ExtractStateFromSetupStore<SS>,
    _ExtractGettersFromSetupStore<SS>,
    _ExtractActionsFromSetupStore<SS>
  >,
): StoreDefinition<
  Id,
  _ExtractStateFromSetupStore<SS>,
  _ExtractGettersFromSetupStore<SS>,
  _ExtractActionsFromSetupStore<SS>
>;
export function defineStore(
  idOrOptions: any,
  setup?: any,
  setupOptions?: any,
): StoreDefinition {
  let id: string;
  let options:
    | DefineStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >
    | DefineSetupStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >;

  // ...
  // 上述省略部分代码主要是做 Id 还是 option 的判断

  // 主要关注这部分
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // 判断下是否已经有全局 state,如果有,获取到它
    const hasContext = hasInjectionContext();
    pinia =
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (hasContext ? inject(piniaSymbol, null) : null);
    if (pinia) setActivePinia(pinia);
    pinia = activePinia!;

    // 如果 id 没有重复,新建 store
    if (!pinia._s.has(id)) {
      // 判断 store 的 形式,如果是 setup 形式定义,则采用 createSetupStore 创建
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia);
      } else {
        // option 形式定义次采用 createOptionsStore 形式创建
        createOptionsStore(id, options as any, pinia);
      }
    }

    // 现在获取全局 state 中的 store
    const store: StoreGeneric = pinia._s.get(id)!;

    // 返回 store
    return store as any;
  }

  useStore.$id = id;

  // 返回 useStore 这个函数,当我们在组件中调用时,获取到其返回值 store
  return useStore;
}
WJ-Yuan commented 1 year ago

createOptionsStore

option store 的整体思路是将 option 的选项转化到 setup 函数里,然后调用 createSetupStore 实现,具体的转化是

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> {
  // 解构 options
  const { state, actions, getters } = options;
  // 初始 state
  const initialState: StateTree | undefined = pinia.state.value[id];

  let store: Store<Id, S, G, A>;

  function setup() {
    if (!initialState && (!__DEV__ || !hot)) {
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {});
      } else {
        // 将 state 的值赋值给 value
        pinia.state.value[id] = state ? state() : {};
      }
    }

    // 将 state 转为 ref
    const localState =
      __DEV__ && hot
        ? toRefs(ref(state ? state() : {}).value)
        : toRefs(pinia.state.value[id]);

    return assign(
      localState,
      actions, // action 不变
      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;
      }, {} as Record<string, ComputedRef>), // 将 getters 按顺序转为 computed 且不可修改
    );
  }

  // 调用 createSetupStore
  store = createSetupStore(id, setup, options, pinia, hot, true);

  return store as any;
}
WJ-Yuan commented 1 year ago

createSetupStore

现在来看 createSetupStore,它是整个 Pinia 创建 store 的核心,其核心思想就是将 setup 中的数据放入到一个 effectScope 中,使其响应

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> {
  let scope!: EffectScope;

  const partialStore = {
    _p: pinia,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {},
    $dispose,
  } as _StoreWithState<Id, S, G, A>;

  const store: Store<Id, S, G, A> = reactive(partialStore);
  // 将其加入pinia
  pinia._s.set($id, store);

  const runWithContext =
    (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext;

  // 创建一个 effect scope 来监测其中的变化
  const setupStore = pinia._e.run(() => {
    scope = effectScope();
    return runWithContext(() => scope.run(setup));
  })!;

  // 合并 store 和 setup store
  assign(store, setupStore);

  return store;
}
WJ-Yuan commented 1 year ago

storeToRefs

解构 store,实现思路就是将其中的每一项都使用 toRef / toRefs 转为响应式

export function storeToRefs<SS extends StoreGeneric>(
  store: SS,
): StoreToRefs<SS> {
  if (isVue2) {
    // vue2 版本直接用 toRefs
    return toRefs(store);
  } else {
    // vue3 版本避免污染原始数据,先将 store 转为不响应
    store = toRaw(store);
    // 新建一个空白对象
    const refs = {} as StoreToRefs<SS>;
    // 循环遍历 store 属性
    for (const key in store) {
      // 取值
      const value = store[key];
      // toRef 响应
      if (isRef(value) || isReactive(value)) {
        refs[key] = toRef(store, key);
      }
    }

    return refs;
  }
}