在对象创建期间使用异步操作时 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>
有趣的是,如果在创建过程中调用异步操作,对象的反应性似乎会完全丧失。
我的样本包括:
- 一个简单的 class,在创建时在构造函数或工厂函数中执行异步操作。
- 一个 Vue 应用程序,在操作挂起时应显示“正在加载...”,并在操作完成后显示操作结果。
- 一个按钮,用于将加载标志设置为 false,并将结果手动设置为静态值
- 部分注释掉以介绍其他方法
观察:
- 如果在 class 本身(构造函数或工厂函数)中等待承诺,实例的反应性将完全中断,即使您手动设置数据(通过使用按钮)
- 在 Vue 组件中调用 awaitPromise 一切正常。
我想避免的替代解决方案:如果 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>
我正在使用我的 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>
有趣的是,如果在创建过程中调用异步操作,对象的反应性似乎会完全丧失。
我的样本包括:
- 一个简单的 class,在创建时在构造函数或工厂函数中执行异步操作。
- 一个 Vue 应用程序,在操作挂起时应显示“正在加载...”,并在操作完成后显示操作结果。
- 一个按钮,用于将加载标志设置为 false,并将结果手动设置为静态值
- 部分注释掉以介绍其他方法
观察:
- 如果在 class 本身(构造函数或工厂函数)中等待承诺,实例的反应性将完全中断,即使您手动设置数据(通过使用按钮)
- 在 Vue 组件中调用 awaitPromise 一切正常。
我想避免的替代解决方案:如果 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>