在对象创建期间使用异步操作时 Vue3 反应性丢失

Vue3 reactivity lost when using async operation during object creation

我正在使用我的 TS 代码库中的一些对象 (classes),这些对象在创建后立即执行异步操作。虽然在 Vue 2.x (code sample), reactivity breaks with Vue3 (sample) 中一切都运行良好,没有任何错误。 为了简单起见,这些示例是用JS编写的,但与我在TS中的真实项目一样。

import { reactive } from "vue";
class AsyncData {
  static Create(promise) {
    const instance = new AsyncData(promise, false);

    instance.awaitPromise();

    return instance;
  }

  constructor(promise, immediate = true) {

    // working, but I'd like to avoid using this
    // in plain TS/JS object
    // this.state = reactive({
    //   result: null,
    //   loading: true,
    // });

    this.result = null;
    this.loading = true;
    this.promise = promise;
    if (immediate) {
      this.awaitPromise();
    }
  }

  async awaitPromise() {
    const result = await this.promise;
    this.result = result;
    this.loading = false;

    // this.state.loading = false;
    // this.state.result = result;
  }
}

const loadStuff = async () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("stuff"), 2000);
  });
};

export default {
  name: "App",
  data: () => ({
    asyncData: null,
  }),
  created() {
    // awaiting promise right in constructor --- not working
    this.asyncData = new AsyncData(loadStuff());

    // awaiting promise in factory function
    // after instance creation -- not working
    // this.asyncData = AsyncData.Create(loadStuff());

    // calling await in component -- working
    // this.asyncData = new AsyncData(loadStuff(), false);
    // this.asyncData.awaitPromise();
  },
  methods: {
    setAsyncDataResult() {
      this.asyncData.loading = false;
      this.asyncData.result = "Manual data";
    },
  },
};
<div id="app">
    <h3>With async data</h3>
    <button @click="setAsyncDataResult">Set result manually</button>
    <div>
      <template v-if="asyncData.loading">Loading...</template>
      <template v-else>{{ asyncData.result }}</template>
    </div>
</div>

有趣的是,如果在创建过程中调用异步操作,对象的反应性似乎会完全丧失。

我的样本包括:

观察:

我想避免的替代解决方案:如果 AsyncData 的状态(加载、结果)包装在 reactive() 中,所有 3 种方法都可以正常工作,但我更愿意避免混合 Vue 的反应性进入应用程序视图层之外的普通对象。

请告诉我你的 ideas/explanations,我真的很想知道发生了什么:)

编辑: 我创建了另一个复制品 link,同样的问题,但设置最少:here

我访问了您发布的代码示例并且它正在运行,我观察到这一点:

  • 您有一个 vue 组件,它在其创建挂钩上实例化一个对象。
  • 实例化对象有内部state
  • 你在 vue 组件中使用那个 state 来渲染一些东西。

看起来像这样:

<template>
<main>
    <div v-if="myObject.internalState.loading"/>
      loading
    </div>
    <div v-else>
      not loading {{myObject.internalState.data}}
    </div>
  </main>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'App',
  data(){
    return {
      myObject:null
    }
  },
  created(){
    this.myObject = new ObjectWithInternalState()
  },
});
</script>

ObjectWithInternalState 在实例化并更改其 internalState 时正在执行异步操作,但是当 internalState 是普通对象时,则没有任何反应。这是预期的行为,因为更改 internalState 的任何内部值都不是 myObject 的突变(vue 反应值),但是如果不是对 internalState 使用普通对象,而是使用 reactive 对象(使用组合 API)并且由于您正在访问模板上的该值,因此模板会观察到对该对象所做的所有更改(反应性!!)。如果你不想有mixed的东西那么你需要等待组件中的异步操作。

export default defineComponent({
  name: 'App',
  data(){
    return {
      remoteData:null,
      loading:false
    }
  },
  created(){
    this.loading = true
    // Option 1: Wait for the promise (could be also async/await
    new ObjectWithInternalState().promise
      .then((result)=>{
        this.loading = false
        this.remoteData = result
      })

    // Option 2: A callback
    new ObjectWithInternalState(this.asyncFinished.bind(this))
  },
  methods:{
    asyncFinished(result){
      this.loading = false
      this.remoteData = result
    }
  }
});

我的建议是将所有状态管理移动到一个商店,看看Vuex这是你想要的最佳实践

西娅·阿贝尔,

我认为您遇到的问题可能是因为 Vue 3 以不同方式处理反应性。在 Vue2 中,发送的值有点用附加功能装饰,而在 Vue 3 中,响应是通过 Proxy 对象完成的。因此,如果你执行 this.asyncData = new AsyncData(loadStuff());,Vue 3 可能会用 new AsyncData(loadStuff()) 的响应替换你的 reactive 对象,这可能会失去反应性。

您可以尝试使用嵌套的 属性 如

  data: () => ({
    asyncData: {value : null},
  }),
  created() {
    this.asyncData.value = new AsyncData(loadStuff());
  }

这样您就不会替换对象。虽然这看起来更复杂,但通过使用 Proxies,Vue 3 可以获得更好的性能,但失去了 IE11 的兼容性。

如果你想验证假设,你可以在赋值前后使用isReactive(this.asyncData)。在某些情况下,分配工作不会失去反应性,我没有检查新的 Class.


这里有一个替代解决方案,不会将反应式放入您的 class

  created() {
    let instance = new AsyncData(loadStuff());
    instance.promise.then((r)=>{
      this.asyncData = {
        instance: instance,
        result: this.asyncData.result,
        loading: this.asyncData.loading,
      }
    });
    this.asyncData = instance;
    // or better yet...
    this.asyncData = {
        result: instance.result,
        loading: instance.loading
    }; 
  }

但不是很优雅。最好使状态成为您传递给 class 的对象,它应该适用于 vue 和非 vue 场景。

这可能是这样的

class withAsyncData {
  static Create(state, promise) {
    const instance = new withAsyncData(state, promise, false);
    instance.awaitPromise();

    return instance;
  }

  constructor(state, promise, immediate = true) {
    this.state = state || {};
    this.state.result = null;
    this.state.loading = true;
    this.promise = promise;
    if (immediate) {
      this.awaitPromise();
    }
  }

  async awaitPromise() {
    const result = await this.promise;
    this.state.result = result;
    this.state.loading = false;
  }
}

const loadStuff = async () => {
  return new Promise((resolve) => {
    setTimeout(() => resolve("stuff"), 2000);
  });
};

var app = Vue.createApp({
  data: () => ({
    asyncData: {},
  }),
  created() {
    new withAsyncData(this.asyncData, loadStuff());
    
    // withAsyncData.Create(this.asyncData, loadStuff());
    
    // let instance = new withAsyncData(this.asyncData, loadStuff(), false);
    // instance.awaitPromise();
  },
  methods: {
    setAsyncDataResult() {
      this.asyncData.loading = false;
      this.asyncData.result = "Manual data";
    },
  },
});

app.mount("#app");
<script src="https://unpkg.com/vue@3.0.11/dist/vue.global.prod.js"></script>
<div id="app">
  <div>
    <h3>With async data</h3>
    <button @click="setAsyncDataResult">Set result manually</button>
    <div>
      <template v-if="asyncData.loading">Loading...</template>
      <template v-else>{{ asyncData.result }}</template>
    </div>
  </div>
</div>