解决快速切换页面导致的内容错乱问题

存在一个典型的场景,同时也是常见的面试题。

问题描述

在一个页面中,左侧是文章列表,右侧是文章详情。当用户点击某篇文章时,页面会从后端获取对应文章的内容并在右侧显示。如果用户快速切换点击多个文章,比如先点击 “JavaScript”,随后快速点击 “CSS”,可能出现以下问题:

1、加载顺序错乱:由于网络请求是异步的,"JavaScript" 的请求可能比 "CSS" 的请求先完成,即便用户当前需要显示 "CSS",却看到的是 "JavaScript" 的内容。

2、体验不佳:快速切换文章时,用户期望看到与点击内容一致的结果,如果出现错乱,会导致页面逻辑混乱。

解决这个问题需要确保页面始终显示用户最后点击的内容,即“用户点到哪一篇文章,就实际加载哪一篇”。

具体方法

针对该问题提供两种可行的解决方案:

1、唯一标识符:每次请求文章时生成唯一标识,仅允许当前有效请求更新页面内容,避免旧请求覆盖最新内容。

2、请求取消法:使用 AbortController,在新请求发起时取消旧的未完成请求,从根本上避免加载顺序错乱。

代码实现

问题 🌰 代码
<template>
  <div>
    <ul>
      <li v-for="article in articles" :key="article.id">
        <button @click="handleArticleClick(article.id)">
          {{ article.title }}
        </button>
      </li>
    </ul>
    <div>
      <p v-if="loading">Loading...</p>
      <p v-else>{{ currentArticle }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 模拟文章数据
const articles = [
  { id: 1, title: 'JavaScript', content: 'JavaScript content...' },
  { id: 2, title: 'CSS', content: 'CSS content...' },
  { id: 3, title: 'HTML', content: 'HTML content...' },
];

// 模拟延迟获取文章详情
const fetchArticleContent = async (id) => {
  // 模拟网络延迟
  await new Promise((resolve) => setTimeout(resolve, 2000));
  return articles.find((article) => article.id === id)?.content || 'Not Found';
};

// 状态管理
const currentArticle = ref(null);
const loading = ref(false);

// 点击事件处理
const handleArticleClick = async (id) => {
  loading.value = true;
  const content = await fetchArticleContent(id);
  currentArticle.value = content;
  loading.value = false;
};
</script>

从上到下挨个点击,2秒后,按顺序依次出现,最后到需要展示的 HTML 内容。

解决方法 1:唯一标识法

原理:

通过给每个请求生成一个唯一的标识符(比如:一个递增的数字 or UUID)。每次发送请求时,都会附带这个标识符,然后在请求成功时比较当前请求的标识符,确保页面内容只会更新为最近一次请求的内容。这样,即使之前请求返回了数据,只要它的标识符不是最新的,就会被忽略,从而避免内容错乱。

适用场景:

1、请求数量少:当请求量不大时,管理请求的标识符是一种轻量且高效的方式,不需要复杂的取消操作。

2、无需主动取消请求:不需要关系请求是否已经完成,只关心最终展示的是用户点击的内容,而不必处理是否取消旧请求的问题。

改进代码:

import { ref } from 'vue';

const articles = [
  { id: 1, title: 'JavaScript', content: 'JavaScript content...' },
  { id: 2, title: 'CSS', content: 'CSS content...' },
  { id: 3, title: 'HTML', content: 'HTML content...' },
];

const fetchArticleContent = async (id) => {
  await new Promise((resolve) => setTimeout(resolve, Math.random() * 2000));
  return articles.find((article) => article.id === id)?.content || 'Not Found';
};

const currentArticle = ref(null);
const loading = ref(false);
const currentRequestId = ref(0);

const handleArticleClick = async (id) => {
  const requestId = ++currentRequestId.value; // 生成唯一标识
  loading.value = true;
  const content = await fetchArticleContent(id);
  console.log('~ requestId:', requestId);
  console.log('~ currentRequestId:', currentRequestId.value);
  if (requestId === currentRequestId.value) {
    // 确保只有最新请求生效
    currentArticle.value = content;
    loading.value = false;
  }
};

分析一下为什么可以实现?很简单,打印看一下。

requestId 的值根据 currentRequestId 变化而改变,但是 currentRequestId 是响应式数据,当点击修改它的值时,Vue 会把这个更改标记为需要更新的状态,实际上它的更新是异步的,只有在下一个 tick(也就是事件循环的下一个阶段)时才会反映出来。因此 requestId 的值会比 currentRequestId.value 小,当二者一样,即为最后一次请求。

优点:实现简单,不需要使用额外的复杂逻辑。

缺点:不适合大量请求场景,因为每个请求都需要生成唯一标识符并通过比较来控制,随着请求数量增多,可能会带来一定的性能负担。

解决方法 2:请求取消法

原理:

当发起一个新请求时,主动取消掉当前未完成的请求,确保只有最新的请求会返回并更新页面内容。通过浏览器的 AbortController 来实现,AbortController 提供了一个取消信号,可以让开发者手动取消请求。

适用场景:

1、请求频繁触发:比如在搜索框中,用户每次输入一个字符都会触发一个请求。请求频繁的情景中,取消旧的请求能减少无意义的计算和展示,提示性能。

2、请求取消:如果应用场景中,早起的请求不再有意义(比如用户已更换搜索条件或文章),就需要取消掉这些不必要的请求,避免它们返回后干扰当前的展示。

如何工作:

每次发起新请求时,都会使用 AbortController 来创建一个新的取消信号。发起请求时,将取消信号传入请求中。如果在请求过程中,新的请求被触发,则会调用 abort() 方法取消前一个请求。这样确保页面始终只显示用户当前需要的内容。

改进代码:

import { ref } from 'vue';

const articles = [
  { id: 1, title: 'JavaScript', content: 'JavaScript content...' },
  { id: 2, title: 'CSS', content: 'CSS content...' },
  { id: 3, title: 'HTML', content: 'HTML content...' },
];

const fetchArticleContent = async (id, controller) => {
  try {
    await new Promise((resolve) => setTimeout(resolve, Math.random() * 2000));
    if (controller.signal.aborted) {
      throw new Error('Request Aborted');
    }
    return articles.find((article) => article.id === id)?.content || 'Not Found';
  } catch (error) {
    if (error.name === 'AbortError') {
      console.warn('Request canceled');
    }
    throw error;
  }
};

const currentArticle = ref(null);
const loading = ref(false);
let abortController = null;

const handleArticleClick = async (id) => {
  if (abortController) {
    abortController.abort(); // 取消之前的请求
  }

  abortController = new AbortController();
  const controller = abortController;

  loading.value = true;

  try {
    const content = await fetchArticleContent(id, controller);
    currentArticle.value = content;
  } catch (error) {
    console.error(error.message); // 处理取消的请求
  } finally {
    loading.value = false;
  }
};

依次点击展示为:

优点:能有效避免不必要的请求,尤其是在请求频繁的场景中,避免了“加载顺序错乱”问题。 

缺点:比唯一标识法实现更复杂,需要使用 AbortController 或类似的取消机制。可能会引入更多的异步错误处理逻辑。

总结

1、唯一标识符

适用于请求量较少、无需主动取消请求的场景。通过给每个请求分配一个唯一标识符,保证只有最后一个请求会更新页面内容。

2、请求取消法

适用于请求量较大、请求频繁的场景。通过取消旧的未完成请求,确保只有用户当前的请求会得到响应并更新页面内容。

选择哪种方法取决于实际应用场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值