Skip to content

异步获取数据列表并完成渲染-使用actions与module(Lesson12 2/3) ​

本页对应 Lesson12,共 6 段视频,约 1 小时 33 分。

学习目标 ​

  • 能够使用 Vuex 的 actions 调用异步接口获取数据,并通过 commit 提交 mutation 更新 state,完成数据列表渲染。
  • 能够使用 mapState、mapGetters、mapMutations、mapActions 简写 TodoList 中状态、计算属性、同步方法和异步方法的调用。
  • 能够说明组件挂载后通过 dispatch 触发 action、action 请求接口、mutation 更新状态、视图重新渲染的完整数据流,并能在代码中定位各环节。
  • 能够按业务拆分 Vuex modules,并在组件中正确访问模块状态、调用模块内的 mutation 和 action。
  • 能够使用 Vue Router 配置一级与嵌套路由,实现带导航的页面切换,并为无导航登录页单独配置路由。
  • 能够根据“未登录跳转登录页”的业务要求,在添加商品、加入购物车等操作中完成基础路由跳转与访问控制验证。

知识点 ​

本节要解决的问题:把异步请求从组件搬进 actions ​

为什么需要:如果像之前那样,在组件 mounted 里直接写 axios.get,请求成功后再 commit 更新 Vuex 状态,那么一旦别的组件也要获取同一份列表,就得把这段异步逻辑再复制一遍。Vuex 的 actions 就是用来集中管理“异步行为”的:组件只负责触发,真正请求数据和提交 mutation 的过程放在 store 里。

怎么想:转写稿里反复强调的流程是:组件挂载完毕 → 通过 dispatch 调用 action → action 里发请求 → 拿到响应体后再 commit 调用 mutation → mutation 更新 state → 视图重新渲染。这里注意,转写稿里的“X”“ex”应理解为 actions,“mon/冒险”应理解为 mutations,“comit”应理解为 commit,“第pat”应理解为 dispatch。

怎么做:在 src/store/index.js 中定义 state.items、mutations.UP_ITEMS,并在 actions.getItems 里使用 axios 获取数据,最后 commit("UP_ITEMS", data.items)。本节笔记代码如下:

js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);

const state = {
  items: []
};

const mutations = {
  UP_ITEMS(state, items) {
    state.items = items;
  }
};

const getters = {};

const actions = {
  async getItems({ commit }, q) {
    const { data } = await axios.get(
      "https://api.github.com/search/repositories?q=" + q + "&sort=stars"
    );
    commit("UP_ITEMS", data.items);
  }
};

const store = new Vuex.Store({
  state,
  mutations,
  getters,
  actions
});

export default store;

易错点:actions 的第一个参数一般命名为 context,它里面有 commit、dispatch、state、getters。转写稿里调试时能看到这些属性;其中 state 可以获取,但不建议在 action 里直接修改,否则就绕过了 mutation,违背“各司其职”的集中管理思路。第二个参数是 payload,只能接一个荷载;想传多个参数时要传对象。转写稿里在后面继续写 b,c,d 是收不到的。

组件里如何触发 action:dispatch 与 mapActions ​

为什么需要:action 定义好后,组件必须能调用它。最直接的是 this.$store.dispatch("getItems", "react"),但模板里方法多了以后,写完整路径比较啰嗦,所以 Vuex 提供 mapActions 把 action 映射成组件方法。

怎么想:mapActions(["getItems"]) 相当于在 methods 里生成 getItems(payload){ this.$store.dispatch("getItems", payload) };对象写法 mapActions({ one: "getItems" }) 则生成 one(payload){ this.$store.dispatch("getItems", payload) }。转写稿中最终在 mounted 里调用 this.one("react"),效果与 this.$store.dispatch("getItems","react") 一致。

怎么做:本节 App.vue 代码如下:

vue
<template>
  <div>
    <div v-for="item in $store.state.items" :key="item.id">
      {{ item.full_name }}
    </div>
  </div>
</template>

<script>
import { mapActions } from "vuex";

export default {
  name: "App.vue",
  methods: {
    ...mapActions(["getItems"]),
    ...mapActions({
      one: "getItems"
    })
  },
  mounted() {
    // this.$store.dispatch("getItems", "react");
    // this.getItems("react");
    this.one("react");
  }
};
</script>

易错点:mapActions 要和 methods 结合使用;mapState、mapGetters 才和 computed 结合。转写稿里还提醒:mounted 执行时视图已经渲染完成,请求回来更新 state 后会触发重新渲染,不要把“先渲染再更新”误认为请求失败。调试时可先确认 action 是否被调用、payload 是否为 "react"、commit 是否执行、state.items 是否变化。

小结:actions 的定位 ​

为什么需要:Vuex 管理的不只是普通数据,还包括同步更新方法和异步行为。mutations 负责同步改状态,actions 负责异步流程并最终提交 mutation。

怎么想:组件不直接改 state,而是 dispatch action;action 拿到数据后 commit mutation;mutation 再改 state。

怎么做:按“定义 action → 组件 dispatch → action 请求 → commit → mutation 更新 → 视图渲染”的顺序写。

易错点:action 名、mutation 名要对应;payload 只传一个;不要在 action 里直接改 state;简写映射后要确认方法名没有冲突。

从 TodoList 收束:Vuex 到底在管理什么 ​

这一节先把 TodoList 的主体和底部收尾,再引出 module。转写里反复出现的“我有叉”应纠正为 Vuex,“mon gather”是 mutation,“geers”是 getters,“X / actions”是 actions。
为什么需要:组件之间不想再来回传参,状态要集中管理。
怎么想:把“数据状态、同步修改、派生计算、异步行为”分开。
怎么做:TodoList 中用 state 存列表,用 mutation 改列表,用 getters 算“选中数量/全部数量”,用 actions 放异步请求。
易错点:不要在组件里直接改 state;mutation 只做同步修改;getters 依赖 state 或其他 getters。

actions:为什么异步行为要集中管理 ​

为什么需要:在 mounted 里写 axios.get 再赋值,只能服务当前组件;别的组件也要请求时,代码会重复。
怎么想:Vuex 不只管理字符串、数值,也管理方法和异步行为。异步请求放进 actions,请求完成后再 commit 给 mutation 更新状态。
怎么做:转写中的流程可以整理为:

js
// store/index.js
import { createStore } from 'vuex'
import axios from 'axios'

export default createStore({
  state: {
    items: []
  },
  mutations: {
    opItems(state, items) {
      state.items = items
    }
  },
  actions: {
    async getItems({ commit }, q) {
      const res = await axios.get('接口地址?q=' + q)
      commit('opItems', res.data.items)
    }
  }
})

组件挂载后调用:

js
mounted() {
  this.$store.dispatch('getItems', 'react')
}

调试时先 console.log(res.data),确认响应体里是 data.items 还是直接 items,再决定 commit 传什么。转写里就出现过“先打印响应体,再确认赋值层级”的过程。
易错点:dispatch 第一个参数是 action 名字,第二个参数是 payload;后面再传多个参数是拿不到的,要传对象。只定义 action 不调用,状态不会变。

mapActions 与简写:少写重复 dispatch ​

为什么需要:每次 this.$store.dispatch('getItems') 太长,组件多时重复。
怎么想:mapActions 把 action 映射成组件方法,本质仍是帮你调用 dispatch。
怎么做:

js
import { mapActions } from 'vuex'

export default {
  methods: {
    ...mapActions(['getItems'])
  },
  mounted() {
    this.getItems('react')
  }
}

也可以写成对象形式起别名。转写中强调:mapActions 相当于生成了一个方法,方法内部执行 this.$store.dispatch('getItems', payload)。
易错点:mapActions 要和 methods 合并;mapGetters 要和 computed 合并;mapMutations 也是映射到 methods。不要把 mapActions 放进 computed。

用商品案例引出 module:先搭路由和业务需求 ​

为什么需要:单文件 store 写 TodoList 还行,业务一复杂就会堆在一起。转写最后没有立刻展开完整 module 代码,而是先用“添加商品、商品列表、购物车、个人中心、登录”案例铺垫。
怎么想:登录页没有导航,其他页面有导航,所以用嵌套路由:外层 Index 放导航和 router-view,子路由放业务页面;登录页单独配置。业务上还规定:未登录时进入个人中心、购物车、点击加入购物车/添加商品,要跳登录。
怎么做:路由结构可整理为:

js
const routes = [
  { path: '/login', component: Login },
  {
    path: '/',
    component: Index,
    children: [
      { path: '', component: AddGoods },
      { path: 'goods', component: GoodsList },
      { path: 'cart', component: Cart },
      { path: 'me', component: Me }
    ]
  }
]

Index.vue 中放导航和出口:

html
<router-link to="/" active-class="active">添加商品</router-link>
<router-link to="/goods" active-class="active">商品列表</router-link>
<router-view></router-view>

易错点:子路由要在一级路由组件里写 router-view;router-link 的选中样式用 active-class;路径拼接要准确。这里的需求划分,正是后面按“商品、购物车、用户”拆 module 的依据。本节先完成路由和业务边界,module 的具体拆分再顺着案例继续推进。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. src/store/index.js —— 定义 state / mutations / getters / actions ​

js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);

// 数据状态
const state = {
	items: []
};

// 同步更新状态方法
const mutations = {
	UP_ITEMS(state, items){
		state.items = items;
	}
};

// 计算属性
const getters = {

};

// 异步行为:
// actions是一个对象,对象的属性名即是action的名字,类型是一个函数,
// 该函数可以通过 $store.dispatch('action的名字', payload) 调用。
const actions = {
	// getItems是action的名字
	// 第一个参数一般命名为 context,是一个对象,该对象中拥有store中常用的方法和属性
	// context->state 在此处可以获取,但不建议修改。
	// dispatch:调用actions下的方法
	// commit: 调用mutations下的方法
	// state:获取state下的数据
	// getters:获取getters中定义的计算属性。
	//
	// 第二个参数一般命名为 payload,荷载数据。
	async getItems({ commit }, q){
		// 获取数据
		const { data } = await axios.get("https://api.github.com/search/repositories?q=" + q + "&sort=stars");
		// 更新数据状态
		commit("UP_ITEMS", data.items);
	}
};

const store = new Vuex.Store({
	state,
	mutations,
	getters,
	actions
});

export default store;

关键行与易错点

  • Vue.use(Vuex) 必须在 new Vuex.Store 之前执行,漏写会直接报错。
  • async getItems({ commit }, q) 中用的是解构,第一个参数是 context 对象,只取其中的 commit;写成 getItems(commit, q) 就拿不到方法了。
  • context.state 能拿到数据但不建议直接改(转写稿明确:能修改,但不正规),改状态必须走 commit("UP_ITEMS", ...),否则 DevTools 里看不到状态变更轨迹。
  • commit 里写的 "UP_ITEMS" 必须和 mutations 中定义的方法名完全一致,大小写错一个字母就不生效且不报错。
  • axios 返回的响应体是 res.data,仓库列表在 res.data.items,所以是 const { data } = await axios.get(...) 后再取 data.items。
  • action 的第二个参数 payload 只能有一个,需要传多个值时要包成对象传。

2. src/App.vue —— 组件中 dispatch 触发 action 并渲染 ​

vue
<template>
	<div>
		<div v-for="item in $store.state.items" :key="item.id">
			{{item.full_name}}
		</div>
	</div>
</template>

<script>
// methods结合使用的是:mapMutations,mapActions
// computed结合使用的是:mapState,mapGetters
import {mapActions} from "vuex";
export default {
	name: "App.vue",
	methods:{
		...mapActions(["getItems"]),
		// 相当于:
		// ...{
		//     getItems(payload){
		//         this.$store.dispatch("getItems",payload)
		//     }
		// }

		...mapActions({
			one:"getItems"
		}),
		// 相当于:
		// ...{
		//     one(payload){
		//         this.$store.dispatch("getItems",payload)
		//     }
		// }
	},
	mounted(){
		// 第一个参数是action的名字。作用:调用store->index.js->actions->getItems,传递参数react
		// 1- 未简写
		// this.$store.dispatch("getItems","react");
		// 2- 简写
		// this.getItems("react");
		// 3- 简写
		this.one("react");
	}
}
</script>

<style scoped>
</style>

关键行与易错点

  • mapActions 要展开在 methods 里,mapState / mapGetters 才是展开在 computed 里,放错位置会报 "is not a function"。
  • 三种调用方式等价:this.$store.dispatch("getItems","react")、this.getItems("react")、this.one("react");one 是重命名写法,dispatch 里传的仍然是 "getItems" 这个原名。
  • 在 mounted 里发请求:此时视图已经渲染完成,数据回来 commit 更新 state 后会触发重新渲染,所以模板里不会先有内容——这是正常流程。
  • 模板里直接 $store.state.items 能用,但更规范的是配合 mapState 或 getters;v-for 一定要写 :key="item.id"。
  • ...mapActions(["getItems"]) 和 ...mapActions({one:"getItems"}) 同时写会重复注入同一个 action(笔记中是为演示两种写法),实际项目里二选一。
  • 易错点:dispatch 是调 action,commit 是调 mutation,混用会出现"commit 找不到方法"或"状态不更新"。

3. src/main.js —— 把 store 挂到根实例(缺了它 $store 为 undefined) ​

js
import Vue from "vue";
import App from "./App.vue";
import store from "./store";

Vue.config.productionTip = false;

new Vue({
	store,
	render: h => h(App)
}).$mount("#app");

关键行与易错点

  • new Vue({ store }) 这一步是组件里能用 this.$store / $store.state 的前提,漏写则所有组件内 $store 都是 undefined。
  • import store from "./store" 默认导入的是 store/index.js 里 export default store 的实例,不要再 new Vuex.Store() 一次,否则组件用的和你改的不是同一个仓库。
  • 依赖要装齐:npm i vuex axios,缺 axios 时 action 里会直接报模块找不到。

常见错误 ​

  1. 现象:action 里数据明明拿到了,页面却一直空白,Vue Devtools 里也看不到这次状态变化。 原因:在 action 中直接写 context.state.items = data.items,绕过了 mutation;严格模式下还会直接抛错。 修法:action 只负责"取数据",改状态一律 commit("UP_ITEMS", data.items),让 mutation 做唯一入口。

  2. 现象:this.$store.dispatch("getItems","react") 之后紧接着 console.log(this.$store.state.items),打印出来是空数组。 原因:action 是异步的,dispatch 返回 Promise,后面的同步代码先执行完了。 修法:await this.$store.dispatch(...) 或 .then() 里再取值;渲染层面则依赖 computed/mapState 自动更新,不要在 dispatch 后面直接读状态。

  3. 现象:...mapActions(["getItems"]) 写进 computed 后,模板里调用 getItems() 报 "is not a function"。 原因:mapActions 生成的是方法,mapState/mapGetters 生成的才是计算属性,放错选项就失效。 修法:mapActions 放 methods,mapState/mapGetters 放 computed。

  4. 现象:对象写法 ...mapActions({ getItems: "one" }),调用 this.getItems("react") 提示找不到 action。 原因:键值写反了。键是"组件里的方法名",值是"store 里的 action 名"。 修法:写成 ...mapActions({ one: "getItems" }),组件里用 this.one("react")。

  5. 现象:dispatch("getItems", "react", 1, 20) 传了四个参数,action 里只有第一个能收到。 原因:action 只接收一个 payload,多余实参被丢弃。 修法:打包成对象 dispatch("getItems", { q: "react", page: 1 }),action 里解构 async getItems({commit},{q,page})。

  6. 现象:在 mutation 里写 axios.get(...) 或 setTimeout,数据有时对有时错,Devtools 时间旅行回放后状态乱掉。 原因:mutation 必须是同步纯函数,异步回调触发时 Devtools 已记录完快照。 修法:请求/定时器全部挪到 action,mutation 里只留一行同步赋值。

  7. 现象:this.$store 是 undefined,或 state.items 报 "Cannot read property of undefined"。 原因:main.js 里 new Vue({...}) 忘了传 store,或 state 里没给 items 初始值。 修法:入口挂载 store,state 中给 items: [] 这类兜底初始值,v-for 前不至于炸。

练习 ​

  1. 把笔记里的 getItems 改成支持分页:dispatch("getItems", { q: "react", page: 2 }),action 内拼接到请求 URL,并用 await 在组件里打印"请求完成"的时机,验证异步顺序。

  2. 用 mapActions 的两种写法各实现一遍同一个搜索动作,再故意把键值写反一次,观察控制台报错信息并说出区别。

  3. 给 store 加一个 loading 状态:action 开始时置 true、结束时置 false,页面用 mapState 显示"加载中…",全程不允许在组件里直接改 store 状态。

  4. 用 modules 拆出 goods 与 cart 两个模块,cart 的 action 里需要读取 goods 的列表长度,写出正确的 rootState 取值方式。

  5. 在 todoList 案例中把"清除已完成"从组件里的 filter 改写为 action + mutation 组合,要求点击后状态与视图同步更新且 Devtools 可回放。

原视频对照 ​

#视频时长
1完成主体09:09
2完成底部13:38
3使用简写完成todoList06:42
4渲染数据列表未使用actions11:38
5异步获取数据列表并完成渲染-未使用actions31:44
6搭建路由20:16

小结 ​

  1. 判断代码放哪的硬标准:会不会异步 —— 会异步进 action,只做同步赋值进 mutation,组件永远不直接写 state。

  2. dispatch 是异步的,凡是"调用完立刻读结果"的写法都是错的,要么 await,要么交给 computed 响应式渲染。

  3. mapActions 属于 methods,mapState/mapGetters 属于 computed,这条记不住就会反复踩同一个坑。

  4. 对象形式的映射永远记"键=本地名、值=store 名",两种 map 写法规则一致。

  5. action 只能接一个 payload,需要多参数一律打包成对象,这既解决丢参也让签名可扩展。

  6. context.state 只读不改,它存在的意义是让你在 action 里做判断(如取当前列表长度),而不是替代 mutation。

  7. 用 modules 后,模块内的 state/getters 是局部的,跨模块访问必须走 rootState/rootGetters。

  8. 数据从"组件私有"上移到"仓库集中",换来的不只是复用,还有 Devtools 可回放、可调试——这才是用 Vuex 的真正理由。