详解 VueUse 中 useAsyncState 实现原理

首发于公众号:前端随想录,欢迎关注

useAsyncState

useAsyncState是一个用于管理异步状态的自定义钩子函数。它是你简化异步操作的最佳拍档,就像鱼儿离不开水,雄鹰离不开天空,你老婆离不开你,同时异步也离不开 useAsyncState,它简化了在Vue组件中处理异步操作的过程,如发送网络请求、加载数据或执行其他耗时的任务。

背景

在Vue 3 Composition API中,我们可以使用自定义钩子函数来封装可复用的逻辑。useAsyncState是一个强大而灵活的自定义钩子函数,帮助我们管理异步操作的状态,使代码更简洁、可读性更强。

我们先来看一下我们平时在项目开发的过程中如何使用异步状态:

html 复制代码
<template>
  <div>{{ data }}</div>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { getData } from './api'

const data = ref(null)
const loading = ref(false)
function loadData() {
  loading.value = true
  getData()
    .then(res => {
      data.value = res
    })
    .catch(e => {
      console.log(e)
    })
    .finally(() => {
      loading.value = false
    })
}

onMounted(() => {
  loadData()
})
</script>

在上面的代码中,我们可以看到,我们需要定义一个data变量来存储异步操作的结果,还需要定义一个loading变量来存储异步操作的加载状态,还需要定义一个loadData函数来执行异步操作,当异步操作开始时,我们需要将loading设置为true,当异步操作结束时,我们需要将loading设置为false,当异步操作成功时,我们需要将结果赋值给data,在这段代码中,我们需要定义两个变量和一个函数,来处理异步操作,这样的代码显然不够优雅,我们可以通过自定义钩子函数来优化它。

针对上面的代码,我们不难发现,当我们需要获取异步状态时,需要一个变量来接受获取到的异步状态,有时我们也会需要用到loading属性来判断异步操作是否正在加载,为此我们需要完成 useAsyncState 来实现它的功能,我们下来看一下实现后的代码:

html 复制代码
<template>
  <div>{{ data }}</div>
</template>

<script lang="ts" setup>
import { useAsyncState } from './useAsyncState'
import { getData } from './api'
const { data, loading } = useAsyncState(getData, null) // 传递异步操作的函数和初始值
</script>

对此我们使用短短十行代码,就实现了异步操作的状态管理,比起之前的代码,我们可以看到,我们不需要定义额外的变量和函数,只需要调用useAsyncState函数,就可以获取异步操作的状态,这样的代码更加简洁,可读性更强。

目的

useAsyncState 旨在提供以下功能:

  • 方便管理异步操作的状态
  • 处理异步操作的加载中、成功和错误等不同状态
  • 支持自定义操作,如在成功或失败时执行其他逻辑
  • 提供性能优化选项,避免不必要的更新

基础设计

在实现useAsyncState之前,我们先来初步的设计一下它的结构:

参数

名称 描述 类型 必传 默认值
fn 用于执行异步操作的函数,该函数返回一个 Promise,在调用返回值 execute 时将会执行该函数,将 Promise 结果赋值给 state (...args) => Promise<any> -
initialValue 默认值,fn函数未执行完成之前,state将为默认值,该参数的类型应和 fn 参数返回的 Promise 结果类型相同 Awaited<ReturnType> -

返回值

名称 描述 类型
state 异步操作的状态,初始值为"initialValue",当 fn 返回的Promise状态完成时,将结果赋值给 state Awaited<ReturnType>
loading 异步操作是否正在加载中,初始值为false,当 fn 执行时,状态为loadin为true,Promise状态完成时,loading为false boolean
execute 执行异步操作的函数,调用时会执行 fn 函数,重新获取异步状态 () => void

注:我们最初的设计是为了方便大家的理解,只实现基本的功能,后续我们会对其进行改进,使其更加易用。

实现

有了上面的设计,我们就可以开始实现useAsyncState了。首先,我们需要定义一个useAsyncState函数,该函数接受两个参数,分别是fninitialValue,并返回一个对象,该对象包含多个属性。

typescript 复制代码
import { ref, Ref } from 'vue'

interface UseAsyncStateReturnType<T> {
  state: Ref<T>
  loading: Ref<boolean>
  execute: () => void
}

/**
 * 响应式异步状态管理
 * @param {() => Promise<T>} fn
 * @param {T} initialValue
 * @returns {UseAsyncStateReturnType<T>}
 */
export function useAsyncState<T>(
  fn: () => Promise<T>,
  initialValue: T
): UseAsyncStateReturnType<T> {
  // 用于保存异步状态的 ref,默认为初始值
  const state = ref<T>(initialValue)
  // loading 状态,默认为 false
  const loading = ref<boolean>(false)

  async function execute() {
    // 将 loading 状态设置为 true
    loading.value = true
    try {
      const data = await fn()
    } finally {
      // 将 loading 状态设置为 false
      loading.value = false
    }
  }

  return {
    state,
    loading,
    execute
  }
}

在上面的代码中,我们实现了 useAsyncState 的基本功能,它接受两个参数,分别是 fninitialValue,并返回一个对象,该对象包含我们约定的属性。

此时我们的 useAsyncState 就已经完成了,我们可以在组件中去使用它

html 复制代码
<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="state">Data: {{ state }}</div>
    <div v-else>Error</div>
    <button @click="execute">Execute</button>
  </div>
</template>

<script lang="ts" setup>
  import { useAsyncState } from './useAsyncState'

  const { state, loading, execute } = useAsyncState(
    () => new Promise(resolve => setTimeout(() => resolve('data'), 1000)),
    []
  )

  execute()
</script>

在使用过程中,我们发现useAsyncState还有一些不足之处:

  1. 需要手动调用execute函数,才能执行异步操作
  2. 调用 execute 函数时,无法传递参数
  3. 无法处理异步操作的错误
  4. 如果我们需要再响应成功和失败的情况下,执行不同的操作,就需要在execute函数中添加额外的逻辑

为了解决上面的问题,我们可以对useAsyncState进行改进,使其更加易用。

改进

功能改进

我们针对上面的问题,梳理一下我们的解决方案:

  1. 添加 immediate 参数,用于控制是否立即执行异步操作
  2. 在返回值中添加 error 属性,用于存储异步操作的错误信息
  3. 添加 onSuccessonError 参数,用于在异步操作成功和失败时执行额外的操作
  4. 在调用 execute 函数时,无法传递参数,我们可以将 execute 函数改为接受一个参数,该参数为 fn 函数的参数

当我们改进后,useAsyncState 的结构如下:

typescript 复制代码
import { ref, Ref } from 'vue'

interface UseAsyncStateReturnType<T, P extends any[]> {
  state: Ref<T>
  loading: Ref<boolean>
  execute: (...args: P) => void
  error: Ref<unknown>
}

interface UseAsyncStateOptions<T> {
  immediate?: boolean
  onSuccess?: (data: T) => void
  onError?: (e: unknown) => void
}

export function useAsyncState<T, P extends any[]>(
  fn: (...args: P) => Promise<T>,
  initialValue: T,
  options: UseAsyncStateOptions<T> = {}
): UseAsyncStateReturnType<T, P> {
  // 解构 options 参数
  const { immediate = false, onError, onSuccess } = options

  // 函数执行结果,默认为初始值
  const state = ref<T>(initialValue) as Ref<T>
  // loading 状态,默认为 false
  const loading = ref<boolean>(false)

  const error = ref<unknown>(null)

  async function execute(...args: any[]) {
    // 在执行异步动作之前将 error 设置为 null
    error.value = null
    // 将 loading 状态设置为 true
    loading.value = true
    try {
      const data = await fn(...(args as P))
      onSuccess?.(data)
    } catch (e: unknown) {
      error.value = error
      onError?.(e)
    } finally {
      // 将 loading 状态设置为 false
      loading.value = false
    }
  }

  if (immediate) {
    // 如果 immediate 为 true,则立即执行异步操作
    execute()
  }

  return {
    state,
    loading,
    execute,
    error
  }
}

在上面的代码中,我们添加了 error 属性,用于存储异步操作的错误信息,当异步操作成功时,我们会将 error 设置为 null,当异步操作失败时,我们会将 error 设置为错误信息。

性能优化

在上述代码中,我们将 state 定义为 Ref 类型,但是 Ref 是一个深度的响应式对象,在大部分情况下,我们使用 useAsyncState 获取到的数据只是用来做展示,所以我们应该避免使用 Ref,而是使用 ShallowRef 来代替,ShallowRef 它只会在修改ref.value时才会触发更新,而不会在修改ref.value的属性时触发更新。

typescript 复制代码
import { ref, Ref, shallowRef } from 'vue'

interface UseAsyncStateReturnType<T, P extends any[]> {
  state: Ref<T>
  loading: Ref<boolean>
  execute: (...args: P) => void
  error: Ref<unknown>
}

interface UseAsyncStateOptions<T> {
  immediate?: boolean
  onSuccess?: (data: T) => void
  onError?: (e: unknown) => void
  shallow?: boolean
}

export function useAsyncState<T, P extends any[]>(
  fn: (...args: P) => Promise<T>,
  initialValue: T,
  options: UseAsyncStateOptions<T> = {}
): UseAsyncStateReturnType<T, P> {
  // 解构 options 参数
  const { immediate = false, shallow = true, onError, onSuccess } = options

  // 函数执行结果,默认为初始值
  const state = (shallow ? ref : shallowRef)<T>(initialValue) as Ref<T>
  // loading 状态,默认为 false
  const loading = ref<boolean>(false)

  const error = shallowRef<unknown>(null)

  async function execute(...args: any[]) {
    // 在执行异步动作之前将 error 设置为 null
    error.value = null
    // 将 loading 状态设置为 true
    loading.value = true
    try {
      const data = await fn(...(args as P))
      onSuccess?.(data)
    } catch (e: unknown) {
      error.value = error
      onError?.(e)
    } finally {
      // 将 loading 状态设置为 false
      loading.value = false
    }
  }

  if (immediate) {
    // 如果 immediate 为 true,则立即执行异步操作
    execute()
  }

  return {
    state,
    loading,
    execute,
    error
  }
}

至此,我们的 useAsyncState 就已经完成了。

使用

我们可以在组件中去使用 useAsyncState,来获取异步数据

html 复制代码
<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="state">Data: {{ state }}</div>
    <div v-else>Error</div>
    <button @click="execute">Execute</button>
  </div>
</template>

<script lang="ts" setup>
  import { useAsyncState } from './useAsyncState'

  const { state, loading, execute } = useAsyncState(
    () => new Promise(resolve => setTimeout(() => resolve('data'), 1000)),
    '',
    {
      immediate: true,
      onSuccess: data => console.log(data),
      onError: error => console.log(error)
    }
  )
</script>

我们来看一下它的效果

总结

在本篇文章中,我们实现了一个 useAsyncState,它可以帮助我们更加方便的获取异步数据,同时也可以帮助我们处理异步操作的错误,以及在异步操作成功和失败时执行额外的操作。

相关推荐
王解10 分钟前
一篇文章读懂 Prettier CLI 命令:从基础到进阶 (3)
前端·perttier
乐闻x16 分钟前
最佳实践:如何在 Vue.js 项目中使用 Jest 进行单元测试
前端·vue.js·单元测试
遇到困难睡大觉哈哈28 分钟前
JavaScript面向对象
开发语言·javascript·ecmascript
檀越剑指大厂30 分钟前
【Python系列】异步 Web 服务器
服务器·前端·python
我是Superman丶32 分钟前
【前端】js vue 屏蔽BackSpace键删除键导致页面后退的方法
开发语言·前端·javascript
Hello Dam34 分钟前
基于 Spring Boot 实现图片的服务器本地存储及前端回显
服务器·前端·spring boot
小仓桑36 分钟前
利用 Vue 组合式 API 与 requestAnimationFrame 优化大量元素渲染
前端·javascript·vue.js
Hacker_xingchen36 分钟前
Web 学习笔记 - 网络安全
前端·笔记·学习
天海奈奈37 分钟前
前端应用界面的展示与优化(记录)
前端
多多*1 小时前
后端并发编程操作简述 Java高并发程序设计 六类并发容器 七种线程池 四种阻塞队列
java·开发语言·前端·数据结构·算法·状态模式