javascript - 如何使用 watchEffect() 防止中止的异步请求破坏状态

标签 javascript vue.js vuejs3 fetch-api vue-composition-api

假设我们有一个组件通过 prop 接收资源的 ID,resourceId .组件应该从 API 获取相应的资源并显示它,同时还处理加载/错误状态(遵循类似于 this one 的模式)。
从外部 API 获取资源的函数也返回 abort如果调用该函数,则会导致请求立即拒绝。当resourceId更改,任何进行中的请求都应中止以支持新请求。对于它的值(value),我正在使用 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>
这在大多数情况下都可以正常工作,但只要取消播放就会中断。如果 resourceId在最后一个 API 请求完成之前发生更改,会发生以下事件顺序:
  • abort()被调用
  • watchEffect回调被调用,设置 loading , error , 和 data
  • catchfinally调用来自原始请求的 block ,设置 loadingerror
  • 第二个 API 请求完成并设置 loadingdata

  • 这会导致意外状态,其中 loading设置为 false当第二个请求进行中时,error包含由中止第一个请求引发的异常,以及 data包含来自第二个请求的值。
    是否有任何设计模式或解决方法可以解决此问题?

    最佳答案

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

  • 第一个 watchEffect
  • abort
  • 第二watchEffect
  • 捕获最后的第一个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>

    关于javascript - 如何使用 watchEffect() 防止中止的异步请求破坏状态,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72637898/

    相关文章:

    typescript - Vue 导入组件与动态组件一起使用的正确类型

    vue.js - 从非 vue 文件访问 vue 3 应用实例 api

    javascript - 如何在 Vue.js 3 中使用 Vue 3 Meta?

    javascript - 如何在函数定义中使用没有 "this"的 bind() 方法?

    javascript - Vuetify动态创建v-select

    android - Android WebView 中的 VueJS (Xamarin)

    vue.js - vuejs 计算的给定 Prop 的 setter 不是响应式(Reactive)的

    javascript - 使用 session 变量和javascript使 session 过期

    javascript - 如何从 jQuery 表单中获取所有输入字段?

    javascript - HTML : Fastest/Best way to update contents of many cells in a large table (jQuery or JavaScript)