Vue 3源码极客之:`Pinia`的`actions`:如何实现异步操作和`dispatch`。

嘿,各位未来的Pinia大师们,准备好开启今天的“Pinia探秘之旅”了吗?今天咱们要聊的是Pinia的actions,这可是store的核心动力引擎,也是咱们跟后端老大哥“眉来眼去”的关键桥梁。

开场白:Actions,Store的“行动派”

在Pinia的世界里,state就像是咱们精心打理的“家”,mutations是“家庭内部事务调整员”(虽然在Pinia里已经淡化了mutations的概念),而actions,就是负责“出门挣钱养家”的那个。 Actions里封装了咱们与外部世界交互的逻辑,比如发起API请求,处理用户输入等等。

一、Actions:定义与基本用法

首先,咱们来看看如何定义一个action。 actions是一个对象,它的每个属性都是一个函数,这些函数就是咱们定义的action。

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    reset() {
      this.count = 0
    }
  }
})

在这个例子中,incrementdecrementreset都是actions。 它们可以直接修改state,是不是很方便?

二、Actions中的this:指向Store实例

在action函数中,this指向的是Store的实例。 这意味着你可以通过this访问state,调用其他actions,甚至访问store的$patch方法来批量更新state。

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    age: 0
  }),
  actions: {
    updateName(newName) {
      this.name = newName
    },
    incrementAge() {
      this.age++
    },
    resetUser() {
      this.$patch({  // 使用$patch批量更新state
        name: '',
        age: 0
      })
    },
    greet() {
      console.log(`Hello, my name is ${this.name}, and I am ${this.age} years old.`);
    },
    complexAction() {
      this.updateName('New Name');
      this.incrementAge();
      this.greet();
    }
  }
})

三、异步Actions:与后端老大哥的“甜蜜互动”

重头戏来了! 异步actions是actions的灵魂,它让咱们能够发起网络请求,处理Promise,异步更新state。

import { defineStore } from 'pinia'

export const usePostStore = defineStore('post', {
  state: () => ({
    posts: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchPosts() {
      this.loading = true
      this.error = null
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts')
        this.posts = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    }
  }
})

在这个例子中,fetchPosts是一个异步action。 它使用async/await来发起网络请求,并在请求成功后更新state。 同时,它还处理了loading状态和错误状态。

四、Actions中的参数:灵活应对各种场景

Actions可以接收参数,这使得咱们可以根据不同的用户输入来执行不同的逻辑。

import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  actions: {
    addItem(item) {
      this.items.push(item)
    },
    removeItem(itemId) {
      this.items = this.items.filter(item => item.id !== itemId)
    },
    updateQuantity(itemId, quantity) {
      const item = this.items.find(item => item.id === itemId)
      if (item) {
        item.quantity = quantity
      }
    }
  }
})

addItemremoveItemupdateQuantity都接收参数,使得咱们可以灵活地操作购物车中的商品。

五、Actions的返回值:传递异步操作的结果

Actions可以返回值,这在某些场景下非常有用,比如需要将异步操作的结果传递给组件。

import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null
  }),
  actions: {
    async login(username, password) {
      try {
        const response = await fetch('/api/login', {
          method: 'POST',
          body: JSON.stringify({ username, password })
        })
        const data = await response.json()

        if (response.ok) {
          this.user = data.user
          this.token = data.token
          return true; // 返回登录成功
        } else {
          return false; // 返回登录失败
        }
      } catch (error) {
        console.error('Login error:', error);
        return false; // 返回登录失败
      }
    },
    logout() {
      this.user = null
      this.token = null
    }
  }
})

login action返回一个布尔值,表示登录是否成功。 组件可以根据返回值来更新UI。

六、Actions的错误处理:让应用更健壮

在异步actions中,错误处理至关重要。 咱们可以使用try...catch块来捕获错误,并在catch块中更新state或显示错误信息。 在上面的fetchPosts的例子中,咱们已经看到了错误处理的例子。

七、Actions与$patch:批量更新State的利器

$patch是Pinia提供的一个方法,用于批量更新state。 它接收一个对象或一个函数作为参数。

  • 接收对象:
this.$patch({
  name: 'New Name',
  age: 30
})
  • 接收函数:
this.$patch((state) => {
  state.name = 'New Name'
  state.age = 30
})

$patch在某些场景下比直接修改state更高效,尤其是在需要更新多个state属性时。

八、Actions中的Actions: Action嵌套调用

Actions 可以互相调用,这使得我们可以将复杂的逻辑拆分成更小的、可重用的action。

import { defineStore } from 'pinia'

export const useTaskStore = defineStore('task', {
  state: () => ({
    tasks: [],
    completedTasks: 0
  }),
  actions: {
    addTask(task) {
      this.tasks.push(task);
    },
    completeTask(taskId) {
      const task = this.tasks.find(task => task.id === taskId);
      if (task) {
        task.completed = true;
        this.incrementCompletedTasks(); // 调用另一个 action
      }
    },
    incrementCompletedTasks() {
      this.completedTasks++;
    },
    removeAllTasks() {
      this.tasks = [];
      this.resetCompletedTasks();
    },
    resetCompletedTasks() {
      this.completedTasks = 0;
    }
  }
})

在上面的例子中, completeTask action 调用了 incrementCompletedTasks action。 这样可以保持代码的模块化和可维护性。

九、dispatch的概念:与Vuex的区别

在Vuex中,我们使用 dispatch 来触发 actions。 但是在 Pinia 中,我们直接调用 action 函数。 Pinia的设计哲学是更简洁,更直观。 不需要 dispatch,直接调用函数,就像调用普通方法一样。

<template>
  <button @click="counterStore.increment()">Increment</button>
</template>

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

const counterStore = useCounterStore()
</script>

这里的 counterStore.increment() 就是直接调用 action 函数。 Pinia 简化了状态管理的流程,降低了学习成本。

十、进阶技巧:使用$onAction进行Action的Hook

Pinia提供了$onAction方法,允许我们注册action的hook,在action执行前后执行一些逻辑。 这在需要进行日志记录、性能监控等场景下非常有用。

import { defineStore } from 'pinia'

export const useAnalyticsStore = defineStore('analytics', {
  state: () => ({
    events: []
  }),
  actions: {
    trackEvent(eventName, payload) {
      this.events.push({ eventName, payload, timestamp: Date.now() })
      console.log(`Tracking event: ${eventName}`, payload)
    }
  }
})

// 在组件或setup函数中使用
import { useAnalyticsStore } from './stores/analytics'
import { useCounterStore } from './stores/counter'
import { onMounted } from 'vue'

export default {
  setup() {
    const analyticsStore = useAnalyticsStore()
    const counterStore = useCounterStore()

    counterStore.$onAction(({
      name, // action 的名字
      store, // store 实例, 也就是 `this`
      args,  // 传递给 action 的参数
      after, // action 成功执行后执行
      onError // action 抛出异常后执行
    }) => {
      const startTime = Date.now()
      // 这将在 action 完成 *后* 执行
      after((result) => {
        const endTime = Date.now()
        analyticsStore.trackEvent(`Action ${name} completed`, { duration: endTime - startTime, result })
      })

      // 如果 action 抛出异常
      onError((error) => {
        analyticsStore.trackEvent(`Action ${name} failed`, { error })
      })
    })

    onMounted(() => {
      counterStore.increment() // 触发 action
    })

    return {}
  }
}

$onAction接收一个回调函数,该函数接收一个对象作为参数,包含了action的名称、store实例、参数、以及afteronError方法。

  • after方法接收一个回调函数,该函数将在action成功执行后执行。
  • onError方法接收一个回调函数,该函数将在action抛出异常后执行。

十一、最佳实践:Actions的设计原则

  • 单一职责: 每个action应该只负责一个明确的任务。
  • 可重用性: 尽量将通用的逻辑封装成可重用的action。
  • 幂等性: 对于某些action,多次执行的结果应该与执行一次的结果相同。
  • 错误处理: 充分考虑各种错误情况,并进行适当的处理。

总结:Actions,Pinia的灵魂

Actions是Pinia的灵魂,它连接了咱们的应用和外部世界。 掌握actions的使用,就能轻松地处理异步操作,管理用户输入,并构建健壮的应用。

Q&A环节

各位同学,有没有什么问题? 欢迎提问,我会尽力解答。


常见问题解答(Q&A)

  1. 问:如果一个action需要调用多个API,应该怎么组织代码?

    答:可以将每个API调用封装成一个单独的函数,然后在action中调用这些函数。 这样可以提高代码的可读性和可维护性。

    async function fetchUserData(userId) {
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    }
    
    async function fetchUserPosts(userId) {
      const response = await fetch(`/api/users/${userId}/posts`);
      return response.json();
    }
    
    export const useUserStore = defineStore('user', {
      state: () => ({
        user: null,
        posts: []
      }),
      actions: {
        async loadUserData(userId) {
          try {
            this.user = await fetchUserData(userId);
            this.posts = await fetchUserPosts(userId);
          } catch (error) {
            console.error('Failed to load user data', error);
            // Handle error
          }
        }
      }
    });
  2. 问:如何在action中访问其他store的state或actions?

    答:可以使用useStore()方法来获取其他store的实例,然后访问其state或actions。

    import { defineStore } from 'pinia'
    import { useCartStore } from './cartStore'
    
    export const useProductStore = defineStore('product', {
      state: () => ({
        products: []
      }),
      actions: {
        addProductToCart(productId) {
          const cartStore = useCartStore()
          const product = this.products.find(product => product.id === productId)
          if (product) {
            cartStore.addItem(product)
          }
        }
      }
    })
  3. 问:如何测试actions?

    答:可以使用单元测试框架(如Jest)来测试actions。 可以mock API请求,并断言state是否按照预期更新。

  4. 问:如何在 Vue 组件中使用 actions 的返回值?

    答:直接调用 action,然后使用 .then()await 来处理返回值 (如果 action 是异步的)。

    <template>
      <button @click="loginUser">Login</button>
      <p v-if="loginSuccess">Login Successful!</p>
      <p v-if="loginError">Login Failed!</p>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import { useAuthStore } from './stores/auth';
    
    const authStore = useAuthStore();
    const loginSuccess = ref(false);
    const loginError = ref(false);
    
    const loginUser = async () => {
      const success = await authStore.login('username', 'password'); // 调用 action
      if (success) {
        loginSuccess.value = true;
        loginError.value = false;
      } else {
        loginSuccess.value = false;
        loginError.value = true;
      }
    };
    </script>
  5. 问:$reset() 有什么用?

    答:$reset() 用于将 store 的 state 重置为初始值。 这对于在某些场景下清理 store 的状态非常有用,例如用户登出时。 $reset() 会调用 store 定义中的 state 函数来获取初始状态。

结束语

希望今天的讲座对大家有所帮助。 记住,实践是检验真理的唯一标准。 多写代码,多踩坑,才能真正掌握Pinia的actions。 祝大家早日成为Pinia大师! 下课!

发表回复

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