Vue中的`AbortController`/`AbortSignal`:实现`watch`与异步操作的生命周期同步

Vue中的AbortController/AbortSignal:实现watch与异步操作的生命周期同步

大家好,今天我们来深入探讨一个在Vue开发中经常被忽视但却至关重要的主题:如何利用AbortControllerAbortSignal来实现watch与异步操作的生命周期同步。尤其是在处理复杂的用户交互和数据驱动的界面时,确保异步操作能够及时取消,避免资源浪费和潜在的副作用至关重要。

背景:异步操作与组件生命周期

在Vue应用中,我们经常需要在组件的watch监听器中执行异步操作,例如:

  1. API 请求: 当监听的属性发生变化时,发起网络请求获取数据。
  2. 定时任务: 根据属性变化,启动或停止定时器。
  3. 计算密集型任务: 当属性变化时,执行复杂的计算,例如数据转换或图像处理。

然而,这些异步操作可能会在其生命周期内完成,即使组件已经被卸载或监听的属性已经改变。这会导致以下问题:

  • 内存泄漏: 异步操作持续运行,即使结果不再需要,占用系统资源。
  • 竞态条件: 多个异步操作并发执行,可能导致结果的顺序与预期不符,影响UI状态。
  • 意外的副作用: 异步操作修改了已卸载组件的状态,导致错误或崩溃。

例如,考虑以下场景:一个搜索框,用户输入关键词后,我们通过watch监听器发起API请求,展示搜索结果。如果用户快速输入多个关键词,可能会发起多个API请求。如果前一个请求尚未完成,但用户已经输入了新的关键词,那么前一个请求的结果就已经过时,但仍然会更新UI,导致显示错误的结果。

AbortControllerAbortSignal简介

AbortControllerAbortSignal是JavaScript提供的一组API,用于控制Web请求和其它异步操作的取消。它们提供了一种优雅的方式来终止正在进行的异步任务,从而避免上述问题。

  • AbortController: 一个控制器对象,用于创建AbortSignal实例,并提供abort()方法来触发取消信号。
  • AbortSignal: 一个信号对象,与特定的异步操作相关联。它包含一个aborted属性,指示是否已发出取消信号。异步操作可以通过监听AbortSignalabort事件或检查aborted属性来判断是否需要停止执行。

watch监听器中使用AbortController

为了解决watch监听器中异步操作的生命周期同步问题,我们可以将AbortControllerwatch监听器结合使用。基本步骤如下:

  1. 创建AbortController实例:watch监听器开始时,创建一个新的AbortController实例。
  2. 关联AbortSignal:AbortControllersignal属性(即AbortSignal实例)传递给异步操作。
  3. 取消之前的操作: 在发起新的异步操作之前,调用abort()方法取消之前正在进行的异步操作。
  4. 监听abort事件或检查aborted属性: 在异步操作中,监听AbortSignalabort事件或定期检查aborted属性,以便在收到取消信号时停止执行。

下面是一个示例,展示如何在Vue组件的watch监听器中使用AbortController来取消API请求:

<template>
  <div>
    <input type="text" v-model="keyword" placeholder="Search...">
    <ul>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      keyword: '',
      results: [],
      abortController: null,
    };
  },
  watch: {
    keyword(newKeyword) {
      if (this.abortController) {
        this.abortController.abort(); // 取消之前的请求
      }

      this.abortController = new AbortController(); // 创建新的 AbortController

      this.search(newKeyword, this.abortController.signal)
        .then(results => {
          if (!this.abortController.signal.aborted) {
            // 检查是否已被取消
            this.results = results;
          } else {
            console.log('Search request aborted.');
          }
        })
        .catch(error => {
          if (error.name === 'AbortError') {
            console.log('Search request aborted:', error.message);
          } else {
            console.error('Search request failed:', error);
          }
        });
    },
  },
  methods: {
    async search(keyword, signal) {
      const response = await fetch(`https://api.example.com/search?q=${keyword}`, { signal });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    },
  },
};
</script>

在这个示例中,我们定义了一个keyword数据属性,并通过watch监听器来监听其变化。当keyword发生变化时,我们首先检查是否存在之前的abortController,如果存在,则调用abort()方法取消之前的请求。然后,我们创建一个新的AbortController实例,并将其signal属性传递给search方法。

search方法中,我们将signal传递给fetch API。fetch API会监听AbortSignalabort事件。当调用abortController.abort()时,AbortSignal会触发abort事件,fetch API会抛出一个AbortError异常。

then回调中,我们检查abortController.signal.aborted属性,以确保请求没有被取消。如果请求被取消,则不更新results数据属性。

catch回调中,我们检查error.name是否为AbortError,以区分取消错误和其它错误。

更通用的封装

为了方便在多个组件中使用AbortController,我们可以将其封装成一个混入(mixin)或一个可复用的函数。

混入 (Mixin):

const abortableWatch = {
  data() {
    return {
      abortControllers: {},
    };
  },
  methods: {
    abortPrevious(watchKey) {
      if (this.abortControllers[watchKey]) {
        this.abortControllers[watchKey].abort();
        delete this.abortControllers[watchKey];
      }
    },
    createAbortController(watchKey) {
      this.abortPrevious(watchKey);
      this.abortControllers[watchKey] = new AbortController();
      return this.abortControllers[watchKey].signal;
    },
  },
  beforeUnmount() {
    // 组件卸载时取消所有未完成的请求
    for (const key in this.abortControllers) {
      this.abortControllers[key].abort();
    }
  },
};

export default abortableWatch;

然后,在Vue组件中使用这个混入:

<template>
  <div>
    <input type="text" v-model="keyword" placeholder="Search...">
    <ul>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>

<script>
import abortableWatch from './abortableWatchMixin';

export default {
  mixins: [abortableWatch],
  data() {
    return {
      keyword: '',
      results: [],
    };
  },
  watch: {
    keyword(newKeyword) {
      const signal = this.createAbortController('keywordSearch'); // 传递一个唯一的 key
      this.search(newKeyword, signal)
        .then(results => {
          if (!signal.aborted) {
            this.results = results;
          } else {
            console.log('Search request aborted.');
          }
        })
        .catch(error => {
          if (error.name === 'AbortError') {
            console.log('Search request aborted:', error.message);
          } else {
            console.error('Search request failed:', error);
          }
        });
    },
  },
  methods: {
    async search(keyword, signal) {
      const response = await fetch(`https://api.example.com/search?q=${keyword}`, { signal });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    },
  },
};
</script>

可复用的函数:

export function useAbortableWatch() {
  const abortControllers = {};

  const abortPrevious = (watchKey) => {
    if (abortControllers[watchKey]) {
      abortControllers[watchKey].abort();
      delete abortControllers[watchKey];
    }
  };

  const createAbortController = (watchKey) => {
    abortPrevious(watchKey);
    abortControllers[watchKey] = new AbortController();
    return abortControllers[watchKey].signal;
  };

  const onBeforeUnmount = () => {
    // 组件卸载时取消所有未完成的请求
    for (const key in abortControllers) {
      abortControllers[key].abort();
    }
  };

  return {
    abortPrevious,
    createAbortController,
    onBeforeUnmount,
  };
}

然后,在Vue组件中使用这个函数:

<template>
  <div>
    <input type="text" v-model="keyword" placeholder="Search...">
    <ul>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>

<script>
import { useAbortableWatch,  onBeforeUnmount, onUnmounted } from 'vue';

export default {
  setup() {
    const { createAbortController, onBeforeUnmount } = useAbortableWatch();

    onUnmounted(onBeforeUnmount); // 确保组件卸载时取消所有请求

    return { createAbortController };
  },
  data() {
    return {
      keyword: '',
      results: [],
    };
  },
  watch: {
    keyword(newKeyword) {
      const signal = this.createAbortController('keywordSearch'); // 传递一个唯一的 key
      this.search(newKeyword, signal)
        .then(results => {
          if (!signal.aborted) {
            this.results = results;
          } else {
            console.log('Search request aborted.');
          }
        })
        .catch(error => {
          if (error.name === 'AbortError') {
            console.log('Search request aborted:', error.message);
          } else {
            console.error('Search request failed:', error);
          }
        });
    },
  },
  methods: {
    async search(keyword, signal) {
      const response = await fetch(`https://api.example.com/search?q=${keyword}`, { signal });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    },
  },
};
</script>

使用函数式组件的setup()中,需要使用onUnmounted来调用onBeforeUnmount,确保在组件卸载时,所有未完成的请求都会被取消。

其他使用场景

除了API请求,AbortController还可以用于取消其他类型的异步操作,例如:

  • 定时器: 使用setTimeoutsetInterval创建的定时器。
  • Web Worker: 在Web Worker中运行的计算密集型任务。
  • Promise: 使用Promise.raceAbortSignal来设置Promise的超时时间。

下面是一个使用AbortController取消定时器的示例:

import { ref, onMounted, onBeforeUnmount } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const abortController = ref(null);

    const startTimer = () => {
      abortController.value = new AbortController();
      const signal = abortController.value.signal;

      const intervalId = setInterval(() => {
        if (signal.aborted) {
          clearInterval(intervalId);
          console.log('Timer aborted.');
          return;
        }
        count.value++;
      }, 1000);
    };

    const stopTimer = () => {
      if (abortController.value) {
        abortController.value.abort();
      }
    };

    onMounted(startTimer);
    onBeforeUnmount(stopTimer);

    return {
      count,
      stopTimer,
    };
  },
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button @click="stopTimer">Stop Timer</button>
    </div>
  `
};

使用AbortController的注意事项

  • 兼容性: AbortController API在现代浏览器中得到了广泛的支持。但是,对于旧版本的浏览器,可能需要使用polyfill。
  • 错误处理: 当异步操作被取消时,通常会抛出一个AbortError异常。需要适当地处理这个异常,以避免程序崩溃。
  • 资源释放: 即使异步操作被取消,也需要确保释放所有相关的资源,例如清除定时器或关闭数据库连接。

总结:正确使用AbortController,同步Vue组件生命周期

这篇文章详细介绍了如何在Vue的watch监听器中使用AbortControllerAbortSignal来同步异步操作的生命周期。通过使用AbortController,我们可以避免内存泄漏、竞态条件和意外的副作用,从而提高应用程序的性能和稳定性。通过使用混入或者可复用的函数,可以避免代码的重复使用,减少代码的维护成本。希望这篇文章能够帮助大家更好地理解和使用AbortController API。

更多IT精英技术系列讲座,到智猿学院

发表回复

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