Pinia 核心源码简易实现

一、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。

javascript 复制代码
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 属性和组件都会自动更新。

vbnet 复制代码
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 实例,并对其进行修改或增强。

javascript 复制代码
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 调用,并记录相关的上下文信息。

javascript 复制代码
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 的状态,并在页面加载时恢复。

javascript 复制代码
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 钩子捕获并处理这些错误。

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

三、总结

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

最后简易版本实现:

ini 复制代码
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 };
}
相关推荐
Nan_Shu_61415 小时前
学习: Threejs (2)
前端·javascript·学习
G_G#15 小时前
纯前端js插件实现同一浏览器控制只允许打开一个标签,处理session变更问题
前端·javascript·浏览器标签页通信·只允许一个标签页
@大迁世界15 小时前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路15 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug15 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213816 小时前
React面向组件编程
开发语言·前端·javascript
持续升级打怪中16 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路16 小时前
GDAL 实现矢量合并
前端
hxjhnct16 小时前
React useContext的缺陷
前端·react.js·前端框架
前端 贾公子16 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端