Pinia 核心源码简易实现

23 阅读3分钟

一、Pinia 的设计理念

Pinia 是一个专为 Vue.js 设计的状态管理库,其设计目标是提供一种更轻量级且类型友好的状态管理解决方案。以下是 Pinia 的主要设计理念:

1. 简洁性

Pinia 的 API 设计非常简洁,易于学习和使用。它摒弃了 Vuex 中的一些复杂概念(如 mutations),仅保留 actions 和 getters。

2. 类型友好

Pinia 完全支持 TypeScript,并且在设计时就考虑了类型推导,使得开发者可以享受到更好的类型提示和安全性。

3. 模块化

Pinia 支持模块化设计,每个 store 都是一个独立的模块,可以单独创建、管理和使用,避免了传统全局状态树的复杂性。

4. 可扩展性

Pinia 提供了插件系统,允许开发者通过插件来扩展 store 的功能,比如添加持久化存储、日志记录等功能。

5. 性能优化

Pinia 利用了 Vue 3 的响应式系统,确保状态更新的高效性和最小的性能开销。

二、Pinia 核心源码分析

1. 创建 Store

Pinia 使用 defineStore 函数来定义 store。这个函数返回一个可组合的函数,用于在组件中使用 store。

import { defineStore } from 'pinia'

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

在内部,defineStore 实际上是在创建一个 reactive 对象,并将其包装成一个可组合函数。Pinia 利用了 Vue 3 的 reactivecomputed 来实现响应式状态和计算属性。

2. 响应式系统

Pinia 依赖于 Vue 3 的响应式系统来追踪依赖并更新视图。当 store 的状态发生变化时,所有依赖该状态的 computed 属性和组件都会自动更新。

import { reactive, computed } from 'vue'

function createReactiveStore(options) {
  const state = reactive(options.state())
  const getters = {}

  for (const key in options.getters) {
    getters[key] = computed(() => options.getters[key](state))
  }

  const actions = {}
  for (const key in options.actions) {
    actions[key] = options.actions[key].bind({ ...state, ...getters, ...actions })
  }

  return {
    ...state,
    ...getters,
    ...actions,
  }
}

3. 插件系统

Pinia 提供了一个灵活的插件系统,允许开发者通过插件来扩展 store 的功能。插件可以通过 useStore 方法访问到 store 实例,并对其进行修改或增强。

export function myPlugin() {
  return (store) => {
    store.$onAction(({ name, store, args, onError, after }) => {
      console.log(`Action ${name} is triggered with args:`, args)
      
      after(() => {
        console.log(`Action ${name} has finished`)
      })

      onError((error) => {
        console.error(`Action ${name} failed with error:`, error)
      })
    })
  }
}

// 在创建 pinia 时使用插件
const pinia = createPinia().use(myPlugin())

插件机制的核心在于 use 方法,它接受一个插件函数作为参数,并将其应用到所有的 store 上。

4. Devtools 集成

Pinia 提供了对 Vue Devtools 的深度集成,包括时间旅行调试、动作跟踪等功能。这些功能是通过 $onAction 钩子实现的,它允许开发者监听 store 上的所有 action 调用,并记录相关的上下文信息。

store.$onAction(({ name, store, args, onError, after }) => {
  // 记录 action 调用
  devtools.emit('action-start', { name, args })

  after(() => {
    // 记录 action 结束
    devtools.emit('action-end', { name, result: store.$state })
  })

  onError((error) => {
    // 记录错误
    devtools.emit('action-error', { name, error })
  })
})

5. 持久化存储

虽然 Pinia 本身不直接提供持久化存储的功能,但可以通过插件轻松实现。例如,可以使用 localStorage 来保存 store 的状态,并在页面加载时恢复。

export function persistStatePlugin() {
  return (store) => {
    // 从 localStorage 加载初始状态
    const savedState = localStorage.getItem(store.$id)
    if (savedState) {
      store.$patch(JSON.parse(savedState))
    }

    // 监听状态变化并保存到 localStorage
    store.$subscribe((mutation, state) => {
      localStorage.setItem(store.$id, JSON.stringify(state))
    })
  }
}

6. 错误处理

Pinia 提供了完善的错误处理机制,特别是在 action 执行过程中发生的错误。开发者可以通过 $onAction 钩子捕获并处理这些错误。

store.$onAction(({ name, store, args, onError, after }) => {
  onError((error) => {
    console.error(`Action ${name} failed with error:`, error)
    // 处理错误,例如显示错误提示给用户
  })
})

三、总结

Pinia 的设计理念强调了简洁性、类型友好性和模块化,使其成为 Vue.js 应用程序的理想状态管理方案。通过深入分析其核心源码,我们可以看到 Pinia 如何利用 Vue 3 的响应式系统来实现高效的 state 管理,并通过插件系统提供了丰富的扩展能力。

最后简易版本实现:

class Store {
  constructor(id, options) {
    this.id = id;
    this.state = reactive(options.state());
    this._getters = {};
    this._actions = {};

    // 初始化 getters
    if (options.getters) {
      Object.keys(options.getters).forEach(key => {
        const getterFn = options.getters[key];
        Object.defineProperty(this, key, {
          get: () => getterFn(this.state)
        });
        this._getters[key] = getterFn;
      });
    }

    // 初始化 actions
    if (options.actions) {
      Object.keys(options.actions).forEach(key => {
        const actionFn = options.actions[key].bind(this);
        this[key] = actionFn;
        this._actions[key] = actionFn;
      });
    }
  }

  $reset() {
    this.state = reactive(this.$options.state());
  }

  $subscribe(callback) {
    watch(() => this.state, callback, { deep: true });
  }

  $onAction(listener) {
    return addListener(this, listener);
  }
}

function defineStore(id, options) {
  let store;

  function useStore() {
    if (!store) {
      store = new Store(id, options);
      store.$id = id;
      store.$options = options;
    }
    return store;
  }

  useStore.$id = id;

  return useStore;
}

// 辅助函数
function reactive(obj) {
  return new Proxy(obj, {
    set(target, key, value) {
      target[key] = value;
      console.log(`Updated ${key} to ${value}`);
      return true;
    },
    get(target, key) {
      console.log(`Getting ${key}`);
      return target[key];
    }
  });
}

function watch(source, cb, options = {}) {
  const effect = () => {
    cleanup();
    activeEffect = effect;
    const newValue = source();
    activeEffect = null;
    if (deepCompare(newValue, oldValue)) {
      cb(newValue, oldValue);
      oldValue = newValue;
    }
  };

  let oldValue, cleanup;
  effect();

  return () => {
    cleanup && cleanup();
  };
}

let activeEffect;
function track(target, key) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

const targetMap = new WeakMap();

function deepCompare(a, b) {
  if (a === b) return false;
  return JSON.stringify(a) !== JSON.stringify(b);
}

function addListener(store, listener) {
  const queue = [];
  const unsubscribe = store.$subscribe((mutation, state) => {
    queue.push({ mutation, state });
  });

  const stop = () => {
    unsubscribe();
    queue.length = 0;
  };

  const flush = () => {
    for (const entry of queue) {
      listener(entry.mutation, entry.state);
    }
    queue.length = 0;
  };

  return { stop, flush };
}