跳到主要内容
← 返回文章列表
面试题-基础2026/08/13约 4 分钟读完

面试题:Vue 3 中的派生状态与响应式数据一致性

本题考察 Vue 响应式状态的设计,以及不同响应式 API 的适用场景。

面试题:Vue 3 中的派生状态与响应式数据一致性
文章目录 5

题目

下面是一个商品筛选组件:

import { ref, watch } from 'vue';

interface Product {
  id: number;
  name: string;
  price: number;
}

const products = ref<Product[]>([
  { id: 1, name: 'Keyboard', price: 300 },
  { id: 2, name: 'Mouse', price: 100 },
]);

const keyword = ref('');
const filteredProducts = ref<Product[]>([...products.value]);
const totalPrice = ref(0);

watch(keyword, () => {
  filteredProducts.value = products.value.filter(product =>
    product.name.toLowerCase().includes(keyword.value.toLowerCase()),
  );

  totalPrice.value = filteredProducts.value.reduce(
    (total, product) => total + product.price,
    0,
  );
});

function addProduct(product: Product) {
  products.value.push(product);
}

请回答:

  1. 这段代码可能出现哪些数据不一致问题?分别在什么操作下发生?
  2. 你会如何重构这段代码?请写出核心实现。
  3. 在这个场景中,你选择的响应式 API 和 watch 有什么本质区别?什么情况下才更适合使用 watch?

我的回答

  1. 页面初始化的时候, filteredProducts和totalPrice对不上;
  2. 写一个页面初始化函数,将初始的商品复制给filteredProducts,并计算totalPrice;也可以采用watchEffect API
  3. watchEffect只要内部ref或者reactive变量值有变动,就会自动触发;其实watch价格immediate属性也可以实现,watch还是更适合后发的监听

题解

结论:你识别了初始化问题,也了解 watchEffect 和 immediate,但没有抓住核心设计原则:纯派生状态应优先使用 computed,而不是手动同步多个可写状态。

评分:45/100

  • 正确性:20/40
  • 原理理解:8/20
  • 工程应用:8/20
  • 边界与权衡:3/10
  • 面试表达:6/10

正确点

  • 初始化时 totalPrice 为 0,确实与 filteredProducts 不一致。
  • 知道 watchEffect 能自动追踪其同步执行过程中读取的响应式依赖。
  • 知道 watch 可以通过 immediate: true 初始化执行。

注意:filteredProducts 初始化时没有问题,因为已经复制了 products.value;不一致的是 totalPrice。

遗漏点

执行 addProduct() 后,keyword 没变,所以原来的 watch(keyword) 不会触发:

addProduct({ id: 3, name: 'Monitor', price: 1000 });

此时:

  • products 已更新;
  • filteredProducts 仍是旧数据;
  • totalPrice 仍是旧价格。

另外,filteredProducts 和 totalPrice 都可以被其他代码直接修改,容易破坏一致性。

更合理的重构

import { computed, ref } from 'vue';

const products = ref<Product[]>([
  { id: 1, name: 'Keyboard', price: 300 },
  { id: 2, name: 'Mouse', price: 100 },
]);

const keyword = ref('');

const filteredProducts = computed(() => {
  const normalizedKeyword = keyword.value.toLowerCase();

  return products.value.filter(product =>
    product.name.toLowerCase().includes(normalizedKeyword),
  );
});

const totalPrice = computed(() =>
  filteredProducts.value.reduce(
    (total, product) => total + product.price,
    0,
  ),
);

products 和 keyword 是源状态;另外两个值都可以由源状态计算得出,不需要额外存储。

API 的职责区别

  • computed:描述一个纯派生值,有缓存,适合本题。
  • watchEffect:自动追踪依赖并执行副作用,不适合代替普通的派生计算。
  • watch:明确监听指定数据,适合请求接口、保存数据、写日志等副作用,也适合需要新旧值或精确控制执行时机的场景。

比“watch 适合后发监听”更准确的面试表达是:

能由已有响应式状态计算出来的值优先使用 computed;需要因状态变化执行外部副作用时,再使用 watch 或 watchEffect。

追问

如果 keyword 改变后需要请求服务端搜索商品,而不是在前端过滤,你会选择 computed、watch 还是 watchEffect?为什么?又该如何处理连续输入造成的请求竞态?

这里只有 keyword 一个明确的监听源,因此 watch 更合适:

let latestRequestId = 0;

watch(keyword, (value, _oldValue, onCleanup) => {
  const requestId = ++latestRequestId;
  const controller = new AbortController();

  const timer = setTimeout(async () => {
    try {
      const data = await searchProducts(value, controller.signal);

      if (requestId === latestRequestId) {
        products.value = data;
      }
    } catch (error) {
      if (!(error instanceof DOMException && error.name === 'AbortError')) {
        console.error(error);
      }
    }
  }, 300);

  onCleanup(() => {
    clearTimeout(timer);
    controller.abort();
  });
});

watchEffect 更适合依赖较多、希望自动收集依赖的副作用;异步使用时还要注意,它只追踪第一次 await 之前同步访问的响应式依赖。

面试表达优化

服务端搜索属于由明确状态变化触发的副作用,因此我会使用 watch 监听 keyword。防抖负责降低请求频率,AbortController 负责取消旧请求,递增的 requestId 负责忽略过期响应,从而保证结果一致性。

区分两组概念:

  • computed 管理纯派生值,watch 管理明确依赖产生的副作用。
  • 防抖优化频率,取消请求节省资源,requestId 保证结果正确性。

评论

0 条

评论会在审核后展示,邮箱不会公开。

正在确认登录状态

5000 / 5000