Vue 和 Vuex 最核心的关系可以压缩成一句话:
Vue 负责“把状态画成界面”,Vuex 负责“让多个组件共享并有秩序地修改业务状态”。
```mermaid
flowchart LR
U["用户操作<br/>点击、输入、滚动"] --> E["Vue 组件事件<br/>@click / @input"]
subgraph Vue["Vue:界面与局部交互"]
E --> M["组件 methods"]
LP["局部状态 data<br/>弹窗开关、hover、输入值"]
CP["派生状态 computed<br/>根据已有状态计算"]
WD["watch<br/>状态变化后的副作用"]
UI["template + Virtual DOM<br/>最终界面"]
LP --> CP
CP --> UI
LP --> UI
LP --> WD
end
M -->|"只影响当前组件"| LP
M -->|"业务操作 dispatch"| A
subgraph Vuex["Vuex:共享业务状态中心"]
A["Action<br/>流程编排、异步请求"]
API["API / Service"]
MU["Mutation<br/>唯一同步写入口"]
ST["State<br/>业务事实"]
GT["Getter<br/>共享派生数据"]
A --> API
API --> A
A -->|"commit"| MU
MU --> ST
ST --> GT
end
ST -->|"mapState"| CP
GT -->|"mapGetters"| CP
UI -->|"重新渲染完成"| U
```
一、Vue:状态到界面的映射器
Vue 组件可以理解为一个函数:
界面 = render(当前状态)
组件通常包含五类能力。
props:外部输入
父组件把数据交给子组件:
<vertical-media-card :item="item" />
子组件不应该直接修改 item,而应通知父组件:
this.$emit('click', this.item);
心智模型:
props 向下传递
event 向上传递
data:组件自己的短期状态
适合保存只属于当前组件的状态:
data() {
return {
filterExpanded: false,
keyword: '',
loading: false
};
}
例如输入框内容、下拉框开关、当前 hover 项,不需要放 Vuex。
computed:从状态推导结果
computed 不保存新的业务事实,只根据已有状态计算:
episodes() {
return this.detailFilelist || [];
}
它具有依赖跟踪和缓存:
detailFilelist 没变化
→ 不重新计算 episodes
detailFilelist 变化
→ episodes 失效并重新计算
→ 使用 episodes 的模板重新渲染
这是 Vue 响应式的核心。
methods:响应用户操作
onEpisodeClick(ep) {
this.playEpisode(ep);
}
methods 负责“发生了什么”,但不应该长期保存业务事实。
watch:状态变化后执行副作用
watch: {
currentDetailItem() {
this.fetchDetail();
}
}
computed 用来计算值,watch 用来执行动作。
判断方式:
需要返回一个结果值 → computed
需要请求接口、操作 DOM、启动定时器 → watch/method
二、Vue 为什么能自动更新组件
Vue 在读取响应式数据时,会记录依赖关系:
DetailView 读取 detailFilelist
→ Vue 记录 DetailView 依赖 detailFilelist
detailFilelist 被修改
→ Vue 通知相关组件
→ 重新计算 computed
→ 生成新的 Virtual DOM
→ 与旧 Virtual DOM 比较
→ 只更新必要 DOM
所以业务代码应该修改状态,而不是手动修改页面:
// 推荐
this.SET_CURRENT_DETAIL_ITEM(item);
// 不推荐
document.querySelector('.detail').style.display = 'block';
组件“自由变化”的基础,就是状态变化后由 Vue 重建正确界面。
三、Vuex:整个业务的共享状态机
Vuex 的核心结构是:
State
Action
Mutation
Getter
Module
state:当前业务事实
影视墙中的典型 State:
{
currentDetailItem,
detailLoading,
detailResourceInfo,
detailFilelist,
mediaCatalog,
categoryPagePosition
}
State 只表达“现在是什么”,不表达“怎么变成这样的”。
mutation:修改 State
Mutation 是同步写入口:
SET_CURRENT_DETAIL_ITEM(state, item) {
state.currentDetailItem = item;
}
Mutation 应该简单、同步、可预测。
不应该在 Mutation 中:
- 请求网络。
- 启动定时器。
- 调用 IPC。
- 写复杂业务流程。
action:完成业务流程
Action 负责异步和编排:
开始 loading
→ 请求接口
→ 转换数据
→ commit 详情
→ 结束 loading
例如:
async fetchResourceDetail({ commit }, payload) {
commit('SET_DETAIL_LOADING', true);
const result = await mediaWallDetailService.getDetail(payload);
commit('SET_DETAIL_RESOURCE_INFO', result.resourceInfo);
commit('SET_DETAIL_FILELIST', result.filelist);
commit('SET_DETAIL_LOADING', false);
}
Action 可以调用 API,但最终仍通过 Mutation 修改 State。
getter:全局 computed
Getter 是 Vuex 级别的派生数据:
visibleEpisodes(state) {
return state.detailFilelist.filter(item => item.path);
}
如果结果只被一个组件使用,优先放组件 computed;多个页面都需要时,再放 Getter。
module:业务状态隔离
大型项目不应该只有一个巨大 Store:
Vuex Store
├── mediaHome
├── userInfo
├── downloading
├── uploading
└── tab
每个 Module 管理一个业务领域。
影视墙的数据、Action 和 Mutation集中在 mediaHome module,避免影响下载、上传等其他业务。
四、一次影视墙操作如何经过 Vue 和 Vuex
以点击影视卡片进入详情为例:
1. VerticalMediaCard 捕获 click
2. 子组件 emit('click', item)
3. MediaLibraryView.onCardClick(item)
4. commit SET_CURRENT_DETAIL_ITEM
5. dispatch fetchResourceDetail
6. Vuex Action 请求 resourcefilelist
7. Service/Mapper 返回统一详情模型
8. Action commit detailResourceInfo/detailFilelist
9. DetailView 的 mapState 收到新状态
10. computed 重新计算
11. Vue 更新详情、演员和分集 DOM
播放分集则不同:
分集信息已经在 Vuex
→ DetailView 读取 ep
→ 调用 Previewer
→ 不需要再修改 Vuex
这说明不是所有操作都要经过 Vuex。Vuex 管共享状态,不是通用函数调用中心。
五、状态到底放在哪里
| 状态 | 应放位置 |
|---|---|
| 按钮 hover、下拉框展开 | 组件 data |
| 搜索框当前文字 | 组件 data |
| 父组件传来的卡片 | props |
| 卡片宽度、分页后的展示列表 | computed |
| 当前影视详情 | Vuex state |
| 多组件共享的分类列表 | Vuex state |
| 请求详情流程 | Vuex action |
| 修改详情数据 | Vuex mutation |
| 多页面共享的派生结果 | Vuex getter |
| 跨窗口播放文件 | IPC,不是 Vuex |
| 重启后仍需保存的数据 | 数据库/持久化存储 |
最重要的判断问题是:
这个状态只有当前组件需要吗?
├── 是:data/computed
└── 否:继续判断
多个组件是否需要共享,并且影响业务流程?
├── 是:Vuex
└── 否:props/$emit
六、最容易出现的错误
把所有状态都放 Vuex
结果是 Store 越来越大,简单弹窗也要写 Action、Mutation。
Vuex 应保存业务事实,不保存每个像素级 UI 状态。
组件直接修改 Vuex State
this.$store.state.mediaHome.currentDetailItem = item;
这会破坏数据流。应使用 Mutation:
this.SET_CURRENT_DETAIL_ITEM(item);
Action 请求回来覆盖新页面
用户先打开 A,又打开 B,但 A 最后返回。
正确做法是在写入前确认:
if (state.currentDetailItem.resourceId !== requestedResourceId) {
return;
}
用 watch 维护可以 computed 的数据
如果一个值完全可以根据其他状态计算,就不应该再复制一份到 data,否则两份数据可能不一致。
七、最终心智模型
把 Vue/Vuex 记成一个单向循环:
用户操作
→ 组件事件
→ Action
→ API
→ Mutation
→ State
→ Computed
→ 界面重新渲染
→ 用户看到结果
其中:
- Vue 是显示系统。
- Vuex 是共享状态系统。
- Action 是业务流程。
- Mutation 是状态写入口。
- Computed 是状态到界面的转换。
- Props/Event 是组件之间的接口。
- API、IPC、Previewer 都是 Vue/Vuex 之外的能力边界。
抓住这一条单向数据流,面对再复杂的 Vue 页面,也可以快速定位:事件从哪里来、状态归谁管理、谁有权修改、最后哪些组件会重新渲染。


评论(0)