如何防止以Watcheffect()的损坏状态中流产的异步请求()

发布于 2025-02-07 18:08:15 字数 1757 浏览 2 评论 0 原文

假设我们有一个组件,可以通过道具, resourceid 接收资源的ID。该组件应从API中获取相应的资源并显示它,同时还处理加载/错误状态(遵循类似于这个)。

从外部API获取资源的功能还返回中止函数,如果调用,该函数会导致请求立即拒绝。当 ResourceD 更改时,任何飞行中的请求都应中止,而有利于新请求。对于它的价值,我正在使用 fetch() abortController 为此。

使用VUE 3与构图API一起,我想出了一个看起来像这样的实现:

const loading = ref(false);
const data = ref(null);
const error = ref(null);

watchEffect(async (onCancel) => {
    loading.value = true;
    data.value = error.value = null;

    const { response, abort } = fetchResourceFromApi(props.resourceId);
    onCancel(abort);

    try {
        data.value = await (await response).json();
    } catch (e) {
        error.value = e;
    } finally {
        loading.value = false;
    }
});
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ data }}</div>

在大多数情况下,这种情况正常,但是每当取消播放时都会断裂。如果 ResourceD 在上次API请求完成之前进行更改,则发生以下事件的顺序:

  1. abort()被称为
  2. WatchEffect 呼叫,设置加载错误 data
  3. catch 最后 原始请求的块被调用,设置加载错误
  4. 第二API请求完成并设置加载 data

此导致出乎意料的状态,其中<代码>加载设置为 false 时,第二个请求在飞行中,错误包含通过中止第一个请求而提出的异常, data data < /代码>包含第二个请求中的值。

是否有任何可以解决此问题的设计模式或解决方法?

Say we have a component that receives the ID of a resource through a prop, resourceId. The component should fetch the corresponding resource from an API and display it, while also handling loading/error states (following a pattern similar to this one).

The function that fetches the resource from the external API also returns an abort function which, if called, causes the request to immediately reject. When resourceId changes, any in-flight requests should be aborted in favor of a new request. For what it's worth, I'm using fetch() and AbortController for this.

Using Vue 3 with the composition API, I came up with an implementation that looks something like this:

const loading = ref(false);
const data = ref(null);
const error = ref(null);

watchEffect(async (onCancel) => {
    loading.value = true;
    data.value = error.value = null;

    const { response, abort } = fetchResourceFromApi(props.resourceId);
    onCancel(abort);

    try {
        data.value = await (await response).json();
    } catch (e) {
        error.value = e;
    } finally {
        loading.value = false;
    }
});
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ data }}</div>

This works fine under most circumstances, but breaks whenever the cancellation comes in to play. If resourceId changes before the last API request has finished, the following order of events happens:

  1. abort() gets called
  2. watchEffect callback gets called, setting loading, error, and data
  3. catch and finally blocks from original request are called, setting loading and error
  4. Second API request completes and sets loading and data

This results in an unexpected state where loading is set to false while the second request is in flight, error contains the exception raised by aborting the first request, and data contains the value from the second request.

Are there any design patterns or workarounds that can deal with this problem?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

恍梦境° 2025-02-14 18:08:15

如果事件的顺序如您所述:

  1. 第一个 WATCHEFFECT
  2. 中止
  3. second
  4. catch,最后是第一个 WatchEffect

然后,您可以保留一个计数器并使用它来跟踪飞行请求中的当前内容(实际上是一个请求ID或某些库称为版本)。每次调用 WATCHEFFECT 都被调用,增加计数器并拍摄当前值的快照。检查您的捕获量,最后阻止目前的计数器与您启动的计数器值相同。如果它们不匹配,则意味着您正在处理过时的请求错误,因此您可以跳过错误和加载状态的修改。

const {ref, watchEffect} = Vue;

const App = {
  setup() {
    const resourceId = ref(0);
    const loading = ref(false);
    const data = ref(null);
    const error = ref(null);

    // fake an api fetch method to asynchronously return 
    // a result after 3 seconds.
    const fetchFor = (id) => {
      let abort;
      const response = new Promise((resolve, reject) => {
        abort = () => reject(`request ${id} aborted`);
        setTimeout(() => resolve({msg: "result for " + id}), 3000);
      });
      return {response, abort};
    };


    let version = 0;

    watchEffect(async (onCancel) => {
      version ++;
      const currentVersion = version;
      
      loading.value = true;
      data.value = error.value = null;
      const { response, abort } = fetchFor(resourceId.value);
      onCancel(abort);
      try {
        data.value = await response;
      } catch (e) {
        if (currentVersion === version) {
          error.value = e;
        }
        
      } finally {
        if (currentVersion === version) {
          loading.value = false;
        }
      }
    });

    return {resourceId, loading, data, error};
  }
};
   
const app = Vue.createApp(App);
app.mount("#app");
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
 <button @click="resourceId++">Request</button>
 <div> last request is {{resourceId}} </div>
 <div> loading = {{loading}} </div>
 <div> data = {{data?.msg}} </div>
 <div> error = {{error}} </div>
</div>

if the sequence of events is as you described:

  1. first watchEffect
  2. abort
  3. second watchEffect
  4. catch and finally of the first watchEffect

Then you can keep a counter and use it to track what the current in flight request is (effectively it is a request id, or version as some libraries call it). Every time watchEffect is called, increase the counter and take a snapshot of the current value. Check in your catch and finally block if the present counter is the same as the counter value you started with. In case they mismatch, it means you are handling stale request errors so you can skip the modification of error and loading status.

const {ref, watchEffect} = Vue;

const App = {
  setup() {
    const resourceId = ref(0);
    const loading = ref(false);
    const data = ref(null);
    const error = ref(null);

    // fake an api fetch method to asynchronously return 
    // a result after 3 seconds.
    const fetchFor = (id) => {
      let abort;
      const response = new Promise((resolve, reject) => {
        abort = () => reject(`request ${id} aborted`);
        setTimeout(() => resolve({msg: "result for " + id}), 3000);
      });
      return {response, abort};
    };


    let version = 0;

    watchEffect(async (onCancel) => {
      version ++;
      const currentVersion = version;
      
      loading.value = true;
      data.value = error.value = null;
      const { response, abort } = fetchFor(resourceId.value);
      onCancel(abort);
      try {
        data.value = await response;
      } catch (e) {
        if (currentVersion === version) {
          error.value = e;
        }
        
      } finally {
        if (currentVersion === version) {
          loading.value = false;
        }
      }
    });

    return {resourceId, loading, data, error};
  }
};
   
const app = Vue.createApp(App);
app.mount("#app");
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
 <button @click="resourceId++">Request</button>
 <div> last request is {{resourceId}} </div>
 <div> loading = {{loading}} </div>
 <div> data = {{data?.msg}} </div>
 <div> error = {{error}} </div>
</div>

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文