返回文章
Vue约 4 分钟阅读

Vue 组合式函数:让搜索请求和清理逻辑一起复用

把搜索状态封装成 composable,用 watch 清理、AbortController 和失效标记防止旧请求覆盖新结果。

GUAN前端开发手记
示例文章

收藏保存在此浏览器,可在归档页查看。

文章目录3 个章节

组合式函数不只是把代码搬进另一个文件。它把有状态的逻辑及其生命周期放在一起:组件负责输入和显示,函数负责请求、状态变化与资源释放。一个搜索框很适合练习这种边界。

把状态与请求放进 useSearch

下面的函数接收一个字符串 ref,返回结果、加载状态和错误信息。示例假设后端提供 /api/search?q=关键词,并返回 [{ "id": 1, "title": "文章标题" }] 这样的数组;实际使用时应按接口约定处理数据。

// src/composables/useSearch.js
import { ref, watch } from 'vue'

export function useSearch(query) {
  const items = ref([])
  const loading = ref(false)
  const error = ref('')

  watch(query, (value, _oldValue, onCleanup) => {
    const controller = new AbortController()
    let active = true

    onCleanup(() => {
      active = false
      controller.abort()
    })

    items.value = []
    error.value = ''
    const term = value.trim()
    loading.value = Boolean(term)
    if (!term) return

    void (async () => {
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(term)}`,
          { signal: controller.signal }
        )
        if (!response.ok) throw new Error(`请求失败:${response.status}`)
        const data = await response.json()
        if (!Array.isArray(data)) throw new Error('搜索结果格式不正确')
        if (active) items.value = data
      } catch (cause) {
        if (active) {
          error.value = cause instanceof Error ? cause.message : '请求失败'
        }
      } finally {
        if (active) loading.value = false
      }
    })()
  }, { immediate: true, flush: 'sync' })

  return { items, loading, error }
}

每次关键词变化,上一轮的清理函数会先执行。abort() 尽可能中止网络工作,active 则明确禁止已失效任务写回状态,包含错误和 finally 中的加载状态。否则旧请求结束时,也可能把新请求的“加载中”提前关掉。

这里对单个字符串使用同步监听,让输入变化时立即使旧任务失效。同步监听不会批处理,不适合频繁同步修改的大数组。真实搜索还可以增加防抖,但防抖只控制发起频率,不能替代过期结果保护。

在组件初始化时调用

组件只需要声明关键词并绑定返回的 ref。这个函数应在 <script setup> 中同步调用,让 Vue 将内部监听器关联到当前组件:

状态声明放在函数内部,每次调用都会得到独立的搜索结果。返回普通对象中的多个 ref,可以安全解构;如果把状态移到模块顶层,就会在调用者之间共享,需要明确这是否符合业务预期。

<script setup>
import { ref } from 'vue'
import { useSearch } from './composables/useSearch.js'

const query = ref('')
const { items, loading, error } = useSearch(query)
</script>

<template>
  <label>搜索文章 <input v-model="query" type="search" /></label>
  <p v-if="loading" role="status">正在搜索…</p>
  <p v-else-if="error" role="alert">{{ error }}</p>
  <ul v-else>
    <li v-for="item in items" :key="item.id">{{ item.title }}</li>
  </ul>
</template>

在组件作用域中同步创建的监听器会随组件卸载而停止,已注册的清理也会执行。不应在一个延迟回调里才创建监听器,然后假设框架仍会自动收尾;这类情况需要保存并调用 watch() 返回的停止函数。

其他副作用也要有对称的清理

事件监听、计时器和订阅同样有生命周期。下面的函数在挂载后监听窗口尺寸,并在卸载时移除同一个处理函数;服务端渲染期间不会访问 window:

import { onMounted, onUnmounted, ref } from 'vue'

export function useWindowWidth() {
  const width = ref(null)
  const update = () => { width.value = window.innerWidth }

  onMounted(() => {
    update()
    window.addEventListener('resize', update)
  })
  onUnmounted(() => window.removeEventListener('resize', update))

  return { width }
}

验证时,先快速输入不同关键词,再在请求进行中离开页面。最终结果应对应最后一次输入,离开后不应继续更新组件状态。良好的复用既包含“如何开始”,也包含“什么时候结束”。

继续阅读:Vue 组合式函数与侦听器清理。

读到这里,不妨动手试一试。
把一个概念变成能运行的小实验。

找一篇笔记

输入关键词,或浏览全部文章。

按 Tab 选择文章Esc 关闭