Vue 3源码深度解析之:`Pinia`的`Actions`:它们如何在`store`中被调用和异步处理。

咳咳,各位观众老爷们,大家好!我是今天的主讲人,咱们今天聊点有意思的——Vue 3 源码剖析之 Pinia 的 Actions。

都说 Pinia 是 Vue 状态管理的最佳实践之一,那它的 Actions 到底是怎么回事?它们如何在 store 中被调用,又如何处理异步操作?咱们今天就来扒一扒它的底裤。

首先,咱们得知道 Actions 在 Pinia 中扮演的角色。简单来说,Actions 就像 store 的“方法”,你可以在里面定义一些逻辑,用来修改 store 的状态。与 Mutations 不同,Actions 可以是同步的,也可以是异步的,而且 Actions 内部可以提交其他的 Actions。

一、 Actions 的基本定义和使用

在 Pinia 中,定义 Actions 非常简单,就像在组件中定义 methods 一样。来看个例子:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    loading: false,
  }),
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      this.loading = true;
      await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟异步操作
      this.count++;
      this.loading = false;
    },
    doubleAndIncrement() {
      this.count *= 2;
      this.increment(); // 调用另一个 action
    },
  },
})

在这个例子中,我们定义了一个名为 counter 的 store,它有三个 actions:increment (同步),incrementAsync (异步) 和 doubleAndIncrement (调用另一个 action)。

使用 Actions 也非常简单:

<template>
  <p>Count: {{ counter.count }}</p>
  <button @click="counter.increment()">Increment</button>
  <button @click="counter.incrementAsync()">Increment Async</button>
  <button @click="counter.doubleAndIncrement()">Double and Increment</button>
  <p v-if="counter.loading">Loading...</p>
</template>

<script setup>
import { useCounterStore } from './stores/counter'

const counter = useCounterStore()
</script>

这段代码展示了如何在组件中使用 useCounterStore 获取 store 实例,然后通过 counter.increment() 等方式调用 actions。

二、 Actions 的内部实现:源码解析

接下来,咱们深入 Pinia 源码,看看 Actions 到底是怎么实现的。 这里我们主要关注 defineStore 函数创建 store 时 actions 的处理逻辑。

简化版的 defineStore 函数(只包含 actions 相关部分):

function defineStore(id, options) {
  const { state, actions } = options;

  function useStore() {
    const store = {};

    // 初始化 state (省略)

    // 处理 actions
    if (actions) {
      for (const actionName in actions) {
        const action = actions[actionName];

        // 关键:绑定 this 到 store 实例
        store[actionName] = action.bind(store);
      }
    }

    return store;
  }

  return useStore;
}

这段代码的核心在于 action.bind(store)bind 方法会创建一个新的函数,并将 this 绑定到 store 实例上。 这样,在 action 内部,就可以通过 this 访问 store 的状态和其他 actions。

为什么需要 bind

因为在 JavaScript 中,this 的指向是动态的,取决于函数是如何被调用的。如果不使用 bindthis 可能会指向 undefined 或者其他对象,导致 action 无法访问 store 的状态。

三、 异步 Actions 的处理

Pinia 对异步 Actions 的处理并没有什么特别之处,它只是简单地允许 Actions 返回 Promise 对象。 你可以使用 async/await 语法来编写异步 Actions。

在上面的 incrementAsync 例子中,我们使用了 await new Promise(resolve => setTimeout(resolve, 1000)) 来模拟一个异步操作。 Pinia 会等待 Promise 对象 resolve 后再继续执行后续的代码。

四、 Actions 的 this 类型

在 TypeScript 中,为了获得更好的类型提示,你可以使用 this 的类型注解来指定 action 函数的 this 类型。

import { defineStore } from 'pinia'

interface State {
  count: number
  loading: boolean
}

export const useCounterStore = defineStore('counter', {
  state: (): State => ({
    count: 0,
    loading: false,
  }),
  actions: {
    increment(this: State) {
      this.count++ // 类型安全,this 指向 State
    },
    async incrementAsync(this: State) {
      this.loading = true;
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.count++;
      this.loading = false;
    },
    doubleAndIncrement(this: State & { increment: () => void }) {
      this.count *= 2;
      this.increment(); // 类型安全,可以调用 increment
    },
  },
})

在上面的例子中,我们使用了 this: State 来指定 incrementincrementAsync 函数的 this 类型为 State。 对于 doubleAndIncrement,由于它需要调用 increment action,所以我们使用了 this: State & { increment: () => void } 来指定 this 类型,确保可以访问 increment 函数。

五、 Actions 的高级用法

除了基本用法之外,Actions 还有一些高级用法,例如:

  • 解构 State 和 Actions: 你可以在 action 内部解构 state 和 actions,以简化代码。

    actions: {
      increment() {
        const { count } = this;
        this.count = count + 1;
      },
      doubleAndIncrement() {
        const { count, increment } = this;
        this.count = count * 2;
        increment();
      },
    },
  • 使用 patch 方法批量更新 state: patch 方法可以用来批量更新 state,提高性能。

    actions: {
      updateProfile(profile) {
        this.$patch({
          name: profile.name,
          email: profile.email,
        });
      },
    },
  • 使用 $reset 方法重置 store: $reset 方法可以用来重置 store 到初始状态。

    actions: {
      reset() {
        this.$reset();
      },
    },

六、 Actions 与 Mutations 的比较

特性 Actions Mutations
同步/异步 可以是同步或异步 必须是同步的
this 指向 指向 store 实例 指向 store 实例
作用 执行复杂的业务逻辑,修改 state 直接修改 state
使用场景 处理异步操作、调用其他 actions、批量更新 state 简单的 state 更新,例如计数器自增、开关状态切换

七、 总结

Pinia 的 Actions 提供了一种简单而强大的方式来管理 store 的状态。 通过 bind 方法,Actions 可以访问 store 的状态和其他 actions。 Actions 既可以处理同步操作,也可以处理异步操作。 通过使用 TypeScript 的类型注解,可以提高代码的类型安全性。

总而言之,Actions 是 Pinia 中不可或缺的一部分,理解 Actions 的原理和使用方法对于掌握 Pinia 至关重要。

八、 源码片段

下面是一些关键的 Pinia 源码片段,进一步加深理解:

  1. defineStore 函数中处理 actions 的部分:
function defineStore(id, options) {
  const { state, actions } = options;

  function useStore() {
    const store = reactive({}); // 使用 reactive 创建响应式对象

    // 初始化 state (省略)

    // 处理 actions
    if (actions) {
      for (const actionName in actions) {
        const action = actions[actionName];

        // 关键:绑定 this 到 store 实例
        store[actionName] = action.bind(store);
      }
    }

    return store;
  }

  return useStore;
}
  1. $patch 方法的简化版实现:
function patch(state, partialState) {
  if (typeof partialState === 'function') {
    partialState(state);
  } else {
    Object.assign(state, partialState); // 使用 Object.assign 批量更新 state
  }
}
  1. $reset 方法的简化版实现:
function reset(store) {
  const initialState = store.$options.state(); // 获取初始 state
  Object.assign(store, initialState); // 将 store 重置为初始 state
}

九、 Q&A 环节

好了,讲了这么多,不知道各位听明白了没有? 现在是 Q&A 环节,大家有什么问题可以提出来,我会尽力解答。 比如:

  • "Actions 可以嵌套调用 Actions 吗?" (当然可以,就像 doubleAndIncrement 例子一样)
  • "Actions 里面可以直接修改 state 吗?" (可以,通过 this 访问 state 并修改)
  • "Actions 里面可以访问其他的 store 吗?" (可以,通过 useStore 函数获取其他 store 实例)

希望这次讲座能帮助大家更好地理解 Pinia 的 Actions。 感谢各位的观看! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注