[Vue] 竞态问题

 

竞态问题通常在多线程编程中被提及。

多线程中的竞态问题(Race Condition)是指多个线程或进程在并发执行时,由于对共享资源(如变量、内存、文件等)的访问和修改顺序不确定,导致程序行为不可预测的问题。

竞态问题的关键点在于:

  1. 共享资源:多个线程同时访问并修改同一个资源,如全局变量或内存位置。
  2. 执行顺序不确定:由于线程调度的不确定性,不同线程执行的顺序可能会不同,导致修改共享资源时产生意外结果。

前端工程师可能很少讨论它,但在日常工作中你可能早就遇到过与竞态问题相似的场景,比如下面的例子:

这里我们又一个简单的共享资源finalData,并且有两次对于响应式数据的操作,由于我们设置了监听,所以,每次响应式数据的改变,watch都会运行,那么也就都会触发耗时操作complexOption。这里我故意设置了第二次请求B的相应速度比第一次请求A快。这会导致请求B先于请求A返回。最终导致finalData中存储的会是第一次请求A的结果

<div id="layer1"></div>
<div id="layer2"></div>
<div id="layer3"></div>
<button id="btn1">修改1</button>
<button id="btn2">修改2</button>
<script type="module" src="index.js"></script>


const layer1 = document.querySelector("#layer1");
const layer2 = document.querySelector("#layer2");
const layer3 = document.querySelector("#layer3");
const btn1 = document.querySelector("#btn1");
const btn2 = document.querySelector("#btn2");
 
let count = 0;
// 耗时操作
async function complexOption(n) {
  return new Promise((resolve, reject) => {
    setTimeout(
      () => {
        resolve(n === 1 ? 100 : 200);
      },
      n === 1 ? 2000 : 500
    );
  });
}

let finalData = ref(0);

watch(proxy, async (newValue, oldValue) => {
  const res = await complexOption(count);
  finalData.value = res;
});

effect(() => {
  layer1.innerHTML = proxy.value.name;
  layer3.innerHTML = finalData.value;
});

btn1.addEventListener("click", () => {
  count = 1
  proxy.value.name = "王五";
});

btn2.addEventListener("click", () => {
  count = 2
  proxy.value.name = "赵六";
});
 

但由于请求 B 是后发送的,因此我们认为请求 B 返回的数据才是最新的,而请求 A 则应该被视为过期的,所以我们希望变量 finalData 存储的值应该是由请求 B 返回的结果,而非请求 A 返回 的结果

实际上,请求 A 是副作用函 数第一次执行所产生的副作用,请求 B 是副作用函数第二次执行所产 生的副作用。由于请求 B 后发生,所以请求 B 的结果应该被视为最新的,而请求 A 已经过期了,其产生的结果应被视为无效。通过这种方式,就可以避免竞态问题导致的错误结果。

归根结底,我们需要的是一个让副作用过期的手段。简单来说,如果出现了多次请求,那么在下一次请求之前,我们需要先清除之前的请求。当然,要清除,首先就是要注册,不然你在哪里去找到清除?

因此,我们用cleanup 来存储用户注册的过期的回调函数,但其实我们处理的可以更加简单,让用户在调用watch的时候进行注册onInvalidate函数,这个函数中的回调函数需要赋值给cleanup,其实这个回调函数可以写的很简单,就是设置一个标识expired,当这个标识为true,也就是只要cleanup有值,并且执行了,就标识我们需要忽略上一次的结果。直接保留最后一次执行的结果即可。

function watch(source, cb, options = {}) {
  let getter;

  if (isFunction(source)) {
    getter = source;
  } else {
    getter = () => traverse(source);
  }

  // 定义旧值与新值
  let oldValue, newValue;

  // cleanup用来存储用户注册的过期回调
  let cleanup;
  // 定义onInvalidate函数
  function onInvalidate(fn) {
    cleanup = fn;
  }

  // 将调度函数scheduler内执行的代码单独封装为job函数
  const job = () => {
    newValue = effectFn();
    // 在调用回调函数cb之前,先调用过期回调
    if (cleanup) {
      cleanup()
    }
    // 将onInvalidate作为回调函数的第三个参数,供用户使用
    cb(newValue, oldValue, onInvalidate);
    oldValue = newValue;
  };

  // 使用 effect 注册副作用函数时,开启 lazy 选项,并把返回值存储到
  // effectFn 中以便后续手动调用
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: job,
  });

  if (options.immediate) {
    job();
  } else {
    oldValue = effectFn();
  }
}
 
const proxy = ref(obj);

let count = 0;
// 耗时操作
async function complexOption(n) {
  return new Promise((resolve, _reject) => {
    setTimeout(
      () => {
        resolve(n === 1 ? 100 : 200);
      },
      n === 1 ? 2000 : 500
    );
  });
}

let finalData = ref(0);

watch(proxy, async (newValue, oldValue, onInvalidate) => {
  let expired = false;
  onInValidate(() => {
    expired = true;
  })
  const res = await complexOption(count);

  if (!expired) {
    finalData.value = res;
  }
});

effect(() => {
  layer1.innerHTML = proxy.value.name;
  layer3.innerHTML = finalData.value;
});

btn1.addEventListener("click", () => {
  count = 1
  proxy.value.name = "王五";
});

btn2.addEventListener("click", () => {
  count = 2
  proxy.value.name = "赵六";
});
 

posted @ 2025-05-18 18:57  Zhentiw  阅读(34)  评论(0)    收藏  举报