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 页面,也可以快速定位:事件从哪里来、状态归谁管理、谁有权修改、最后哪些组件会重新渲染。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。