首页 > 网页制作 >Computed聚合依赖与竞态处理

Computed聚合依赖与竞态处理

来源:互联网 2026-07-09 12:11:00

通过将计算属性依赖改为返回字符串,彻底切断了与表单对象的引用关系,解决了Vue3响应式系统中因对象引用变化导致的幽灵触发和死循环问题,同时结合竞态处理避免了异步请求结果错乱。

最近在做一个金融级表单,遇到了一个典型的问题:watch监听直接把页面搞挂了。翻来覆去调试了几轮,总算把坑填平了,这里把排查过程和优化思路梳理一下。

问题现场的代码大致是这样:

长期稳定更新的攒劲资源: >>>点此立即查看<<<

Computed聚合依赖与竞态处理

//计算期权费(名义本金*期权费率(%)*期限/计息基础)请求接口
let calcTimer = null;
watch(
  () => [
    form.value.nominalPrincipal,
    form.value.premiumRate,
    form.value.dayCount,
    form.value.startDate,
    form.value.mtrDate,
  ],
  async (
    [nominalPrincipal, premiumRate, dayCount, startDate, mtrDate]
  ) => {
    if (calcTimer) clearTimeout(calcTimer);
    if (form.value.secondClass === "场内期权") {
      form.value.premiumAmount = null;
      return;
    }
    if (
      !nominalPrincipal ||
      !premiumRate ||
      !dayCount ||
      !startDate ||
      !mtrDate
    ) {
      form.value.premiumAmount = null;
      return;
    }
    calcTimer = setTimeout(async () => {
      const res = await calcPremiumAmount({
        nominalPrincipal,
        premiumRate,
        dayCount,
        startDate,
        mtrDate,
      });
      form.value.premiumAmount = res.data;
    }, 300);
  }
);

第一步改进,加入新旧值的比较,页面就恢复正常了:

//计算期权费(名义本金*期权费率(%)*期限/计息基础)请求接口
let calcTimer = null;
watch(
  () => [
    form.value.nominalPrincipal,
    form.value.premiumRate,
    form.value.dayCount,
    form.value.startDate,
    form.value.mtrDate,
  ],
  async (
    [nominalPrincipal, premiumRate, dayCount, startDate, mtrDate],
    [oldNominalPrincipal, oldPremiumRate, oldDayCount, oldStartDate, oldMtrDate]
  ) => {
    if (
      nominalPrincipal === oldNominalPrincipal &&
      premiumRate === oldPremiumRate &&
      dayCount === oldDayCount &&
      startDate === oldStartDate &&
      mtrDate === oldMtrDate
    )
      return;
    if (calcTimer) clearTimeout(calcTimer);
    if (form.value.secondClass === "场内期权") {
      form.value.premiumAmount = null;
      return;
    }
    if (
      !nominalPrincipal ||
      !premiumRate ||
      !dayCount ||
      !startDate ||
      !mtrDate
    ) {
      form.value.premiumAmount = null;
      return;
    }
    calcTimer = setTimeout(async () => {
      const res = await calcPremiumAmount({
        nominalPrincipal,
        premiumRate,
        dayCount,
        startDate,
        mtrDate,
      });
      form.value.premiumAmount = res.data;
    }, 300);
  }
);

考虑到代码的可读性,又尝试用Computed聚合依赖加上竞态处理,但页面还是挂了:

const premiumCalcDeps = computed(() => ({
  secondClass: form.value.secondClass,
  nominalPrincipal: form.value.nominalPrincipal,
  premiumRate: form.value.premiumRate,
  dayCount: form.value.dayCount,
  startDate: form.value.startDate,
  mtrDate: form.value.mtrDate,
}));
let calcTimer = null;
let currentRequestId = 0;
watch(premiumCalcDeps, async (newVal) => {
  if (calcTimer) clearTimeout(calcTimer);
  const { secondClass, nominalPrincipal, premiumRate, dayCount, startDate, mtrDate } = newVal;
  if (secondClass === "场内期权") {
    form.value.premiumAmount = null;
    return;
  }
  if (!nominalPrincipal || !premiumRate || !dayCount || !startDate || !mtrDate) {
    form.value.premiumAmount = null;
    return;
  }
  calcTimer = setTimeout(async () => {
    const requestId = ++currentRequestId;
    try {
      const res = await calcPremiumAmount({
        nominalPrincipal,
        premiumRate,
        dayCount,
        startDate,
        mtrDate,
      });
      if (requestId === currentRequestId) {
        form.value.premiumAmount = res.data;
      }
    } catch (error) {
      console.error("期权费计算失败:", error);
    }
  }, 300);
});

加上newVal和oldVal的比较后,才算稳定运行:

const premiumCalcDeps = computed(() => ({
  secondClass: form.value.secondClass,
  nominalPrincipal: form.value.nominalPrincipal,
  premiumRate: form.value.premiumRate,
  dayCount: form.value.dayCount,
  startDate: form.value.startDate,
  mtrDate: form.value.mtrDate,
}));
let calcTimer = null;
let currentRequestId = 0;
watch(premiumCalcDeps, async (newVal, oldVal) => {
   if (
    newVal.secondClass === oldVal.secondClass &&
    newVal.nominalPrincipal === oldVal.nominalPrincipal &&
    newVal.premiumRate === oldVal.premiumRate &&
    newVal.dayCount === oldVal.dayCount &&
    newVal.startDate === oldVal.startDate &&
    newVal.mtrDate === oldVal.mtrDate
  ) {
    return;
  }
  if (calcTimer) clearTimeout(calcTimer);
  const { secondClass, nominalPrincipal, premiumRate, dayCount, startDate, mtrDate } = newVal;
  if (secondClass === "场内期权") {
    form.value.premiumAmount = null;
    return;
  }
  if (!nominalPrincipal || !premiumRate || !dayCount || !startDate || !mtrDate) {
    form.value.premiumAmount = null;
    return;
  }
  calcTimer = setTimeout(async () => {
    const requestId = ++currentRequestId;
    try {
      const res = await calcPremiumAmount({
        nominalPrincipal,
        premiumRate,
        dayCount,
        startDate,
        mtrDate,
      });
      if (requestId === currentRequestId) {
        form.value.premiumAmount = res.data;
      }
    } catch (error) {
      console.error("期权费计算失败:", error);
    }
  }, 300);
});

最后一步优化,把计算属性改为返回字符串,彻底切断对象引用:

const premiumCalcDeps = computed(() => {
  return [
    form.value.secondClass,
    form.value.nominalPrincipal,
    form.value.premiumRate,
    form.value.dayCount,
    form.value.startDate,
    form.value.mtrDate,
  ].join("|");
});
let calcTimer = null;
let currentRequestId = 0;
watch(premiumCalcDeps, async (newVal, oldVal) => {
  if (newVal === oldVal) return;
  if (calcTimer) clearTimeout(calcTimer);
  const [secondClass, nominalPrincipal, premiumRate, dayCount, startDate, mtrDate] = newVal.split("|");
  if (secondClass === "场内期权") {
    form.value.premiumAmount = null;
    return;
  }
  if (!nominalPrincipal || !premiumRate || !dayCount || !startDate || !mtrDate) {
    form.value.premiumAmount = null;
    return;
  }
  calcTimer = setTimeout(async () => {
    const requestId = ++currentRequestId;
    try {
      const params = Qs.stringify({
        nominalPrincipal: Number(nominalPrincipal),
        premiumRate: Number(premiumRate),
        dayCount,
        startDate,
        mtrDate,
      });
      const res = await calcPremiumAmount(params);
      if (requestId === currentRequestId) {
        form.value.premiumAmount = res.data;
      }
    } catch (error) {
      console.error("期权费计算失败:", error);
    }
  }, 300);
});

为什么对象比对会出问题?Vue 3的响应式系统里,对象的比对基于内存引用,字符串则基于内容。把依赖项拼成字符串,就彻底切断了与form对象结构的依赖关系,从根本上杜绝了因为对象引用变化引发的“幽灵触发”和死循环。

1. 什么是竞态处理?

在金融或表单业务中,竞态问题通常发生在“异步请求”加“防抖延迟”的组合下。举个例子:你正在修改期权费率(premiumRate),先输入了5,再删掉改成6。

  • 输入5时,触发一个300ms定时器,准备请求接口A(参数5)。
  • 200ms后输入6,触发新的300ms定时器,准备请求接口B(参数6)。

如果没有竞态处理,网络不可控时,接口A(参数5)可能比接口B(参数6)晚返回。最终用户输入的是6,页面却显示了5的结果,这就是竞态导致的数据错乱。

2. 代码执行过程深度解析

// 1. 全局变量,只要组件不销毁,它的值就会一直保留并累加
let currentRequestId = 0;
// 2. 每次 watch 触发(即每次 setTimeout 执行时),都会生成一个新的局部变量
const requestId = ++currentRequestId;

场景:用户快速输入5,然后改成6

  • 第一次触发(输入5):currentRequestId从0变成1,局部变量requestId赋值为1。进入setTimeout准备发请求,这个setTimeout内部的requestId被锁死在1。
  • 第二次触发(输入6):currentRequestId从1变成2,局部变量requestId赋值为2,进入新的setTimeout。此时全局的currentRequestId已经是2了。
if (requestId === currentRequestId) {
  form.value.premiumAmount = res.data;
}
  • 如果接口A(参数5)晚返回:它拿着自己的requestId=1,去和全局currentRequestId=2比较,1===2为false,数据被丢弃。
  • 如果接口B(参数6)正常返回:它拿着requestId=2,和全局的2比较,2===2为true,数据成功写入。

3. 总结

关键在于:局部变量requestId是用const声明的,每次watch执行都会创建全新的、独立的requestId——它就像那次请求的“出生证明”,一旦生成永远不变。而全局的currentRequestId每次触发都自增,相当于当前世界的“最新时间戳”。

竞态处理的本质就是:用“局部的旧快照(requestId)”去对比“全局的新状态(currentRequestId)”,如果不一致,说明已经过时了,直接放弃更新。

这套逻辑在金融级表单里至关重要,它确保用户看到的永远是最后一次操作的结果。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。