注意 Vuex 状态变化!
这是我在 Dev.to 上的第一篇文章,所以非常期待任何反馈,希望它们能帮助我提升整体写作水平,并帮助我补充一些我可能忘记写和解释的内容!第一段写完了,让我们开始 Vue 吧!
今天有人在 Slack 上提问,关于如何在 Vue 组件中处理不同的状态。他想要的是这样的:你发起一个请求,它有 3 个基本状态(待处理/正在加载、成功、失败/错误)。如何在 Vue 组件中处理这个问题?他问可以用 Vuex 来实现(他自己也在用 Vuex),但我先不提这个问题,因为没必要用 Vuex 来实现(不过我也会探索 Vuex 的世界)。
首先,我们有 3 个状态,并且需要针对每个状态做出不同的行为。下面的代码片段展示了一种实现方法:
<template>
<h1 v-if="status === 'success'">Success</h1>
<h1 v-else-if="status === 'error'">Error</h1>
<h1 v-else>Loading</h1>
</template>
它基本上根据状态显示不同的消息,这是所需的行为。
让我们首先假设它是一个单一组件,并且请求的数据不会在其他任何地方(父组件或兄弟组件)需要,这使得方法变得简单(我稍后将探讨其他方法)。
我假设你对 Vue.js 有点熟悉,这意味着你了解created
、methods
和data
。现在让我们为该特定组件实现所需的行为(api.get
模拟延迟 1 秒的 API 请求,以便我们可以看到状态的转换)。
import api from '@/api';
export default {
name: 'simple',
data() {
return {
status: 'pending',
};
},
created() {
console.log(`CREATED called, status: ${this.status}`);
this.handleCreated();
},
methods: {
async handleCreated() {
try {
await api.get();
this.status = 'success';
} catch (e) {
console.error(e);
this.status = 'error';
}
},
},
};
这里没什么大不了的,因为所有操作都在组件内部处理,而提问者的情况并非如此。我想他的情况可能有点不同。在他的例子中,状态需要在其他组件之间共享,而不仅仅是这个组件的子组件。在这种情况下,我们可能会有一个共享状态,这时 Vuex 就派上用场了(你可以用 Vuex 来实现同样的效果Event Bus
,而且它比仅仅为这个状态添加 Vuex 更好)。
现在让我们更新组件,使用来自 Vuex Store 的状态,而不是本地值。首先,我们创建状态。
export default new Vuex.Store({
state: {
status: 'pending',
},
mutations: {
},
actions: {
},
});
现在让我们更新我们的组件以使用state.status
:
<template>
<h1 v-if="status === 'success'">Success</h1>
<h1 v-else-if="status === 'error'">Error</h1>
<h1 v-else>Loading</h1>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'vuex1',
computed: mapState(['status']),
};
</script>
下一步是在调用 API 后更新状态。我们可以像之前一样实现,只需引用Vuex Store 中的状态即可,但这是非常糟糕的做法。现在正确的做法是派发一个 Vuex Action 来处理,因此我们首先创建 Action 来处理它:
export default new Vuex.Store({
state: {
status: 'pending',
},
getters: {
status: state => state.status,
},
mutations: {
updateStatus(state, status) {
Vue.set(state, 'status', status);
},
},
actions: {
async fetchApi({ commit }) {
try {
await api.get();
commit('updateStatus', 'success');
} catch (e) {
console.error(e);
commit('updateStatus', 'error');
}
},
},
});
一旦我们假设状态在其他组件之间共享,并且我们不希望每个组件反复地调度相同的 Action,那么从组件中调度 Action 就毫无意义了。因此,我们会在我们的App.vue
文件或任何其他对您的应用有意义的组件中(例如在视图的主组件中)调度 Action。以下是调度已创建 Action 的文件代码片段App.vue
:
<template>
<div>
<simple />
<vuex1 />
</div>
</template>
<script>
import Simple from '@/components/Simple.vue';
import Vuex1 from '@/components/Vuex1.vue';
export default {
name: 'app',
components: {
Simple,
Vuex1,
},
created() {
this.$store.dispatch('fetchApi');
},
};
</script>
太棒了,现在它按预期工作了!但我什么都没告诉你。他试图解决的问题比这个更深层次。他希望某些受此状态更新的组件在状态发生变化时能够表现不同。想象一下,你可能希望在这个 API 调用成功后为每个组件调度不同的操作,那么,在只调度页面中渲染的组件的操作的情况下,该如何实现这一点呢?
我的目的是向你展示几种处理这种情况的可能性。我事先声明,对于我们大多数人来说,这种情况可能听起来很尴尬,但请尝试抽象一下我向你展示的场景,并专注于你能从我这里展示的功能中实现什么(你可能会遇到完全不同的场景,这个解决方案比这里更合适)。
手表
实现我们期望解决方案的最简单方法。您可以监视属性更新并按您想要的方式处理它。在下面的示例中,我们需要更新一个“复杂”的对象,否则我们的组件将崩溃:
<template>
<h1 v-if="status === 'success'">Success {{ complex.deep }}</h1>
<h1 v-else-if="status === 'error'">Error</h1>
<h1 v-else>Loading</h1>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'vuex2',
data() {
return {
complex: null,
};
},
computed: mapState(['status']),
watch: {
status(newValue, oldValue) {
console.log(`Updating from ${oldValue} to ${newValue}`);
// Do whatever makes sense now
if (newValue === 'success') {
this.complex = {
deep: 'some deep object',
};
}
},
},
};
</script>
Vuex 手表
你知道也可以使用 Vuex 来监听变化吗?这里是文档。唯一的要求是,它监听一个函数,该函数接收 State 作为第一个参数,Getters 作为第二个参数,并返回另一个函数,该函数将监听其结果。
使用 Vuex watch 时需要注意一点:它会返回一个unwatch
函数,如果你想停止 watcher,则应该在你的 hook 中调用该函数beforeDestroy
。如果你不调用这个函数,watcher 仍然会被调用,这并非我们期望的行为。
这里要记住的一件事是,反应性发生在监视回调被调用之前,这意味着我们的组件将在设置复杂对象之前更新,所以我们需要注意这里:
<template>
<h1 v-if="status === 'success'">Success {{ complex && complex.deep }}</h1>
<h1 v-else-if="status === 'error'">Error</h1>
<h1 v-else>Loading</h1>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'vuex3',
data() {
return {
complex: null,
};
},
computed: mapState(['status']),
created() {
this.unwatch = this.$store.watch(
(state, getters) => getters.status,
(newValue, oldValue) => {
console.log(`Updating from ${oldValue} to ${newValue}`);
// Do whatever makes sense now
if (newValue === 'success') {
this.complex = {
deep: 'some deep object',
};
}
},
);
},
beforeDestroy() {
this.unwatch();
},
};
</script>
Vuex 订阅
您可以订阅突变,这意味着每当突变提交时,您的处理程序都会被调用(您可以使用subscribeAction对操作执行相同的操作)。这有点棘手,因为我们不会只订阅特定的突变,所以我们必须在这里小心。
使用 Vuex subscribe 时需要注意一点:它会返回一个unsubscribe
函数,如果你想停止订阅者,则应该在你的 hook 中调用该函数beforeDestroy
。如果你不调用这个函数,订阅者仍然会被调用,这并非我们期望的行为。
这里的缺点是我们丢失了旧值,但由于第一种情况,它在响应式发生之前就被调用了,因此我们避免了重复检查(如果有问题的话)。结果如下代码片段所示:
<template>
<h1 v-if="status === 'success'">Success {{ complex.deep }}</h1>
<h1 v-else-if="status === 'error'">Error</h1>
<h1 v-else>Loading</h1>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'vuex4',
data() {
return {
complex: null,
};
},
computed: mapState(['status']),
created() {
this.unsubscribe = this.$store.subscribe((mutation, state) => {
if (mutation.type === 'updateStatus') {
console.log(`Updating to ${state.status}`);
// Do whatever makes sense now
if (state.status === 'success') {
this.complex = {
deep: 'some deep object',
};
}
}
});
},
beforeDestroy() {
this.unsubscribe();
},
};
</script>
结论
正如我之前提到的,我的想法不仅仅是解决 Slack 那个家伙提出的问题。我想分享一个更广泛的视角,看看有哪些可用的解决方案以及如何使用它们。
你可能遇到不同的问题,这些解决方案可能很合适,但正如我在本文中提到的:保持简单!我一开始就针对一个特定问题提供了一个非常简单的解决方案,你也应该这样做。等到性能问题或重构出现后,再考虑复杂的解决方案。
如果你愿意,你也可以在 Github 上查看它:vue-listen-to-change-example
更新
- 2020年3月23日:已添加
unwatch
/unsubscribe
调用beforeDestroy
。特别感谢@opolancoh在评论中提及。