MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Vuex 如何实现异步数据加载与缓存策略

2024-05-114.2k 阅读

异步数据加载基础

在 Vue 应用中,异步数据加载是非常常见的需求。比如从 API 获取用户信息、商品列表等。通常,我们会使用 async/await 或者 Promise 来处理异步操作。

使用 async/await 进行异步数据加载

在 Vue 组件中,我们可以在 created 钩子函数中发起异步请求。假设我们有一个获取用户信息的 API,示例代码如下:

<template>
  <div>
    <p v-if="user">{{ user.name }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  async created() {
    try {
      const response = await fetch('https://example.com/api/user')
      const data = await response.json()
      this.user = data
    } catch (error) {
      console.error('Error fetching user:', error)
    }
  }
}
</script>

在上述代码中,fetch 函数返回一个 Promise,我们使用 await 等待 Promise 解决(resolved),然后获取响应数据并赋值给 user。如果请求过程中出现错误,我们在 catch 块中进行处理。

使用 Promise 链式调用进行异步数据加载

除了 async/await,我们也可以使用 Promise 的链式调用方式。以下是改写后的代码:

<template>
  <div>
    <p v-if="user">{{ user.name }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  created() {
    fetch('https://example.com/api/user')
    .then(response => response.json())
    .then(data => {
        this.user = data
      })
    .catch(error => {
        console.error('Error fetching user:', error)
      })
  }
}
</script>

这里通过 then 方法来处理 Promise 解决后的结果,catch 方法捕获可能出现的错误。虽然这两种方式都能实现异步数据加载,但 async/await 的代码结构看起来更接近同步代码,可读性更强。

Vuex 中的异步操作

Vuex 是 Vue 的状态管理模式,它有自己的一套机制来处理异步操作。在 Vuex 中,我们通常使用 actions 来处理异步逻辑。

简单的 Vuex Action 示例

假设我们有一个 Vuex 模块用于管理用户信息,以下是一个简单的 actions 示例:

// store/modules/user.js
import axios from 'axios'

const state = {
  user: null
}

const mutations = {
  SET_USER(state, user) {
    state.user = user
  }
}

const actions = {
  async fetchUser({ commit }) {
    try {
      const response = await axios.get('https://example.com/api/user')
      commit('SET_USER', response.data)
    } catch (error) {
      console.error('Error fetching user:', error)
    }
  }
}

export default {
  state,
  mutations,
  actions
}

在上述代码中,fetchUser 是一个异步 action。它使用 axios 发起 HTTP 请求,当请求成功后,通过 commit 调用 mutation SET_USER 来更新状态。mutation 是唯一可以直接修改 Vuex 状态的地方,这保证了状态变化的可追踪性。

复杂异步操作与并发请求

有时候,我们可能需要处理多个异步操作,甚至是并发请求。比如,我们同时需要获取用户信息和用户的订单列表。

// store/modules/user.js
import axios from 'axios'

const state = {
  user: null,
  orders: []
}

const mutations = {
  SET_USER(state, user) {
    state.user = user
  },
  SET_ORDERS(state, orders) {
    state.orders = orders
  }
}

const actions = {
  async fetchUserAndOrders({ commit }) {
    const userPromise = axios.get('https://example.com/api/user')
    const ordersPromise = axios.get('https://example.com/api/orders')

    try {
      const [userResponse, ordersResponse] = await Promise.all([userPromise, ordersPromise])
      commit('SET_USER', userResponse.data)
      commit('SET_ORDERS', ordersResponse.data)
    } catch (error) {
      console.error('Error fetching user or orders:', error)
    }
  }
}

export default {
  state,
  mutations,
  actions
}

fetchUserAndOrders action 中,我们创建了两个异步请求的 Promise,然后使用 Promise.all 来并发执行这两个请求。当两个请求都成功完成后,通过 commit 调用相应的 mutation 更新状态。如果其中任何一个请求失败,Promise.all 会立即失败并进入 catch 块。

异步数据缓存策略

在前端应用中,缓存异步数据可以显著提高应用性能,减少不必要的网络请求。

简单的本地缓存策略

我们可以使用浏览器的 localStorage 来实现简单的数据缓存。在获取数据时,先检查 localStorage 中是否存在缓存数据,如果存在且未过期,则直接使用缓存数据,否则发起网络请求。

<template>
  <div>
    <p v-if="user">{{ user.name }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: null
    }
  },
  async created() {
    const cachedUser = localStorage.getItem('user')
    if (cachedUser) {
      const { data, expires } = JSON.parse(cachedUser)
      if (expires > Date.now()) {
        this.user = data
        return
      }
    }

    try {
      const response = await fetch('https://example.com/api/user')
      const data = await response.json()
      this.user = data
      const expires = Date.now() + 3600000 // 缓存1小时
      localStorage.setItem('user', JSON.stringify({ data, expires }))
    } catch (error) {
      console.error('Error fetching user:', error)
    }
  }
}
</script>

上述代码在 created 钩子函数中,首先检查 localStorage 中是否有用户缓存数据,并且缓存数据是否过期。如果缓存可用,则直接使用缓存数据。否则,发起网络请求获取最新数据,并将数据及过期时间存入 localStorage

Vuex 中的缓存策略

在 Vuex 中实现缓存策略,可以结合 Vuex 的状态持久化插件。例如,使用 vuex-persistedstate 插件。首先安装该插件:

npm install vuex-persistedstate

然后在 Vuex 配置中引入:

import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import userModule from './modules/user'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user: userModule
  },
  plugins: [createPersistedState()]
})

export default store

vuex-persistedstate 插件会将 Vuex 的状态持久化到 localStoragesessionStorage 中。这样,当页面刷新或者重新加载时,Vuex 的状态可以从存储中恢复,实现了一种简单的缓存机制。

基于版本控制的缓存策略

对于一些数据变化较为频繁的场景,我们可以采用基于版本控制的缓存策略。假设 API 提供了数据版本号,每次获取数据时,我们将版本号与缓存中的版本号进行比较。如果版本号相同,则使用缓存数据,否则更新缓存。

// store/modules/user.js
import axios from 'axios'

const state = {
  user: null,
  userVersion: null
}

const mutations = {
  SET_USER(state, { user, version }) {
    state.user = user
    state.userVersion = version
  }
}

const actions = {
  async fetchUser({ commit, state }) {
    try {
      const response = await axios.get('https://example.com/api/user?version=' + state.userVersion)
      const { data, version } = response.data
      if (version === state.userVersion) {
        return state.user
      }
      commit('SET_USER', { user: data, version })
      return data
    } catch (error) {
      console.error('Error fetching user:', error)
    }
  }
}

export default {
  state,
  mutations,
  actions
}

在上述代码中,fetchUser action 会将当前缓存的版本号作为参数传递给 API。如果 API 返回的版本号与缓存版本号相同,则直接返回缓存数据。否则,更新缓存数据和版本号。

缓存更新策略

缓存虽然能提高性能,但当数据发生变化时,需要及时更新缓存,以保证数据的一致性。

手动更新缓存

在一些情况下,我们知道数据发生了变化,比如用户修改了自己的信息。此时可以手动更新缓存。在 Vuex 中,我们可以在更新数据的 mutation 或者 action 中同时更新缓存。

// store/modules/user.js
import axios from 'axios'

const state = {
  user: null
}

const mutations = {
  SET_USER(state, user) {
    state.user = user
    localStorage.setItem('user', JSON.stringify(user))
  }
}

const actions = {
  async updateUser({ commit }, updatedUser) {
    try {
      const response = await axios.put('https://example.com/api/user', updatedUser)
      commit('SET_USER', response.data)
    } catch (error) {
      console.error('Error updating user:', error)
    }
  }
}

export default {
  state,
  mutations,
  actions
}

updateUser action 中,当用户信息更新成功后,通过 commit 调用 SET_USER mutation,在 mutation 中不仅更新 Vuex 状态,还同时更新 localStorage 中的缓存数据。

自动缓存失效

另一种策略是设置缓存的过期时间,让缓存自动失效。比如我们之前在 localStorage 缓存用户信息时设置了 1 小时的过期时间。这种方式适用于数据更新频率不高且对实时性要求不是特别严格的场景。

基于事件的缓存更新

在一些复杂的应用中,可能存在多个模块之间的数据交互。我们可以通过事件总线或者 Vuex 的 subscribe 方法来实现基于事件的缓存更新。例如,当订单模块发生数据变化时,通知用户模块更新缓存。

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import userModule from './modules/user'
import orderModule from './modules/order'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user: userModule,
    order: orderModule
  }
})

store.subscribe((mutation, state) => {
  if (mutation.type === 'order/UPDATE_ORDER') {
    store.dispatch('user/fetchUser')
  }
})

export default store

在上述代码中,通过 store.subscribe 监听所有的 mutation。当 order/UPDATE_ORDER mutation 发生时,触发 user/fetchUser action,重新获取用户信息,从而更新用户模块的缓存。

异步数据加载与缓存的性能优化

在实现异步数据加载和缓存策略后,还需要对性能进行优化,以确保应用的流畅运行。

防抖与节流

在一些用户频繁触发异步操作的场景下,比如搜索框输入时实时搜索,使用防抖(Debounce)和节流(Throttle)技术可以有效减少不必要的请求。

防抖:在一定时间内,多次触发同一事件,只执行最后一次。例如,用户在搜索框输入内容,我们可以设置防抖时间为 300 毫秒,只有当用户停止输入 300 毫秒后才发起搜索请求。

<template>
  <div>
    <input v-model="searchText" @input="debouncedSearch">
    <ul>
      <li v-for="result in searchResults" :key="result.id">{{ result.title }}</li>
    </ul>
  </div>
</template>

<script>
import { debounce } from 'lodash'

export default {
  data() {
    return {
      searchText: '',
      searchResults: []
    }
  },
  methods: {
    async search() {
      try {
        const response = await fetch(`https://example.com/api/search?q=${this.searchText}`)
        const data = await response.json()
        this.searchResults = data
      } catch (error) {
        console.error('Error searching:', error)
      }
    },
    debouncedSearch: debounce(function() {
      this.search()
    }, 300)
  }
}
</script>

在上述代码中,通过 lodashdebounce 方法对 search 方法进行包装,实现了防抖功能。

节流:在一定时间内,无论触发多少次事件,只执行一次。例如,在页面滚动时,我们可以设置每 500 毫秒发起一次获取新数据的请求,避免频繁请求。

<template>
  <div @scroll="throttledFetchMore">
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { throttle } from 'lodash'

export default {
  data() {
    return {
      items: [],
      page: 1
    }
  },
  methods: {
    async fetchMore() {
      try {
        const response = await fetch(`https://example.com/api/items?page=${this.page}`)
        const data = await response.json()
        this.items = [...this.items, ...data]
        this.page++
      } catch (error) {
        console.error('Error fetching more items:', error)
      }
    },
    throttledFetchMore: throttle(function() {
      this.fetchMore()
    }, 500)
  }
}
</script>

这里使用 lodashthrottle 方法对 fetchMore 方法进行包装,实现了节流功能。

预加载

预加载是指在当前数据使用之前,提前加载可能需要的数据。比如在用户浏览商品列表时,我们可以提前加载下一页的商品数据,当用户点击下一页时,数据可以立即显示,提高用户体验。

<template>
  <div>
    <ul>
      <li v-for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
    <button @click="loadNextPage">Next Page</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [],
      currentPage: 1,
      nextPageData: null
    }
  },
  methods: {
    async loadPage(page) {
      try {
        const response = await fetch(`https://example.com/api/products?page=${page}`)
        const data = await response.json()
        return data
      } catch (error) {
        console.error('Error loading page:', error)
      }
    },
    async loadNextPage() {
      if (this.nextPageData) {
        this.products = [...this.products, ...this.nextPageData]
        this.nextPageData = null
        this.currentPage++
        this.preloadNextPage()
      } else {
        const data = await this.loadPage(this.currentPage + 1)
        this.products = [...this.products, ...data]
        this.currentPage++
        this.preloadNextPage()
      }
    },
    async preloadNextPage() {
      this.nextPageData = await this.loadPage(this.currentPage + 1)
    }
  },
  created() {
    this.loadPage(1).then(data => {
      this.products = data
      this.preloadNextPage()
    })
  }
}
</script>

在上述代码中,preloadNextPage 方法在当前页数据加载完成后,提前加载下一页的数据并存储在 nextPageData 中。当用户点击下一页时,如果 nextPageData 有数据,则直接使用,否则再发起请求。

缓存清理与管理

随着应用的运行,缓存数据可能会越来越多,占用大量内存。因此,需要定期清理缓存。对于 localStorage 缓存,可以在应用启动时检查缓存数据的数量或者大小,如果超过一定阈值,则清理部分或全部缓存。

// main.js
const MAX_CACHE_SIZE = 10 // 假设最多缓存10条数据
function cleanLocalStorage() {
  const keys = Object.keys(localStorage)
  if (keys.length > MAX_CACHE_SIZE) {
    const keysToRemove = keys.slice(0, keys.length - MAX_CACHE_SIZE)
    keysToRemove.forEach(key => localStorage.removeItem(key))
  }
}

cleanLocalStorage()

在上述代码中,cleanLocalStorage 函数检查 localStorage 中键的数量,如果超过 MAX_CACHE_SIZE,则删除最早的部分缓存数据。

在 Vuex 中,如果使用了持久化插件,也可以通过在 mutation 或者 action 中控制状态的清理来间接管理缓存。例如,当用户退出登录时,清除用户相关的缓存状态。

// store/modules/user.js
const state = {
  user: null
}

const mutations = {
  SET_USER(state, user) {
    state.user = user
  },
  CLEAR_USER(state) {
    state.user = null
  }
}

const actions = {
  logout({ commit }) {
    commit('CLEAR_USER')
    localStorage.removeItem('user')
  }
}

export default {
  state,
  mutations,
  actions
}

logout action 中,不仅清除 Vuex 中的用户状态,还删除 localStorage 中的用户缓存数据。

通过合理地实现异步数据加载、缓存策略以及性能优化,我们可以打造出高效、流畅的 Vue 应用。在实际项目中,需要根据具体的业务需求和场景,灵活选择和组合这些技术,以达到最佳的用户体验和应用性能。