内容来自尚硅谷教学课件
6.1. vuex 理解
6.1.1. vuex 是什么
- github 站点: https://github.com/vuejs/vuex
- 在线文档: https://vuex.vuejs.org/zh-cn/
- 简单来说: 对vue 应用中多个组件的共享状态进行集中式的管理(读/写)
6.1.2. 状态自管理应用
- state: 驱动应用的数据源
- view: 以声明方式将state 映射到视图
- actions: 响应在view 上的用户输入导致的状态变化(包含n 个更新状态的方法)
6.1.3. 多组件共享状态的问题
- 多个视图依赖于同一状态
- 来自不同视图的行为需要变更同一状态
- 以前的解决办法
a. 将数据以及操作数据的行为都定义在父组件
b. 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递) - vuex 就是用来解决这个问题的
6.2. vuex 核心概念和API
6.2.1. state
- vuex 管理的状态对象
- 它应该是唯一的
const state = {
xxx: initValue
}
6.2.2. mutations
- 包含多个直接更新state 的方法(回调函数)的对象
- 谁来触发: action 中的commit(‘mutation 名称’)
- 只能包含同步的代码, 不能写异步代码
const mutations = {
yyy (state, {data1}) {
// 更新state 的某个属性
}
}
6.2.3. actions
- 包含多个事件回调函数的对象
- 通过执行: commit()来触发mutation 的调用, 间接更新state
- 谁来触发: 组件中: $store.dispatch(‘action 名称’, data1) // ‘zzz’
- 可以包含异步代码(定时器, ajax)
const actions = {
zzz ({commit, state}, data1) {
commit('yyy', {data1})
}
}
6.2.4. getters
- 包含多个计算属性(get)的对象
- 谁来读取: 组件中:
$store.getters.xxx
const getters = {
mmm (state) {
return ...
}
}
6.2.5. modules
- 包含多个module
- 一个module 是一个store 的配置对象
- 与一个组件(包含有共享数据)对应
6.2.6. 向外暴露store 对象
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
6.2.7. 组件中
import {mapState, mapGetters, mapActions} from 'vuex'
export default {
computed: {
...mapState(['xxx']),
...mapGetters(['mmm']),
}
methods: mapActions(['zzz'])
}
{{xxx}} {{mmm}} @click="zzz(data)"
6.2.8. 映射store
import store from './store'
new Vue({
store
})
6.2.9. store 对象
- 所有用vuex 管理的组件中都多了一个属性$store, 它就是一个store 对象
- 属性:
state: 注册的state 对象
getters: 注册的getters 对象 - 方法:
dispatch(actionName, data): 分发调用action
6.3. demo1: 计数器
6.3.1. store.js
/**
* vuex 的store 对象模块
*/
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
/*
state 对象
类似于data
*/
const state = {
count: 0 // 初始化状态数据
}
/*
mutations 对象
包含个方法: 能直接更新state
一个方法就是一个mutation
mutation 只能包含更新state 的同步代码, 也不会有逻辑
mutation 由action 触发调用: commit('mutationName')
*/
const mutations = {
//增加的mutation
INCREMENT(state) {
state.count++
},
//减少的mutation
DECREMENT(state) { // ctrl + shift + x
state.count--
}
}
/*
actions 对象
包含个方法: 触发mutation 调用, 间接更新state
一个方法就是一个action
action 中可以有逻辑代码和异步代码
action 由组件来触发调用: this.$store.dispatch('actionName')
*/
const actions = {
//增加的action
increment({
commit
}) {
//提交增加的mutation
commit('INCREMENT')
},
//减少的action
decrement({
commit
}) {
//提交减少的mutation
commit('DECREMENT')
},
//带条件的action
incrementIfOdd({
commit,
state
}) {
if (state.count % 2 === 1) {
commit('INCREMENT')
}
},
//异步的action
incrementAsync({
commit
}) {
//在action中直接就可以执行异步代码
setTimeout(() => {
commit('INCREMENT')
}, 1000)
}
}
/*
getters 对象
包含多个get 计算计算属性方法
*/
const getters = {
oddOrEven(state) { //不需要调用,只需要读取属性值
return state.count % 2 === 0 ? '偶数' : '奇数'
},
count(state) {
return state.count
}
}
// 向外暴露store 实例对象
export default new Vuex.Store({
//构造函数
state, //状态
mutations, //包含多个更新state函数的对象
actions, //包含多个对应事件回调函数的对象
getters //包含多个getter计算属性函数的对象
})
6.3.2. main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
components: {App},
template: '<App/>',
store // 所有组件都多个一个属性: $store
})
6.3.3. app.vue(未优化前)
<template>
<div>
<p>clicked: {{ $store.state.count }} times, count is {{ oddOrEven }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementIfOdd">increment if odd</button>
<button @click="incrementAsync">increment async</button>
</div>
</template>
<script>
export default {
computed: {
oddOrEven() {
return this.$store.getters.oddOrEven;
}
},
methods: {
increment() {
//通知vuex去增加
this.$store.dispatch("increment"); //触发store中对应的action调用
},
decrement() {
//通知vuex去减少
this.$store.dispatch("decrement"); //触发store中对应的action调用
},
incrementIfOdd() {
this.$store.dispatch("incrementIfOdd");
},
incrementAsync() {
this.$store.dispatch("incrementAsync");
}
}
};
</script>
<style>
</style>
6.3.4. app2.vue(优化后)
<template>
<div>
<p>clicked: {{ count }} times, count is {{ oddOrEven2 }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementIfOdd">increment if odd</button>
<button @click="incrementAsync">increment async</button>
</div>
</template>
<script>
// import { mapState, mapGetters, mapActions } from "vuex";
import { mapGetters, mapActions } from "vuex";
export default {
//使用映射减少重复代码
// computed: {
// ...mapState(['count']),
// ...mapGetters(['evenOrOdd'])
// }
computed: mapGetters({
// 名称不一样
oddOrEven2: "oddOrEven",
count: "count"
}),
methods: mapActions([
"increment",
"decrement",
"incrementIfOdd",
"incrementAsync"
]) // 名称一样
};
</script>
<style>
</style>
6.4. demo2: todo list
6.4.1. store/types.js
/**
* 包含多个mutation name
*/
export const RECEIVE_TODOS = 'receive_todos'
export const ADD_TODO = 'add_todo' //添加todo
export const REMOVE_TODO = 'remove_todo'
export const DELETE_DONE = 'delete_done'
export const UPDATE_ALL_TODOS = 'update_all_todos'
6.4.2. store/mutations.js
//包含多个由action触发去直接更新状态的方法对象
import {
RECEIVE_TODOS,
ADD_TODO,
REMOVE_TODO,
DELETE_DONE,
UPDATE_ALL_TODOS
} from './types'
export default {
[RECEIVE_TODOS](state, {
todos
}) {
state.todos = todos
},
[ADD_TODO](state, {
todo
}) {
state.todos.unshift(todo)
},
[REMOVE_TODO](state, {
index
}) {
state.todos.splice(index, 1)
},
[DELETE_DONE](state) {
state.todos = state.todos.filter(todo => !todo.complete)
},
[UPDATE_ALL_TODOS](state, {
isCheck
}) {
state.todos.forEach(todo => todo.complete = isCheck)
}
}
6.4.3. store/actions.js
// 包含多个接受组件通知触发mutation调用,间接更新状态的方法对象
import storageUtil from '../util/storageUtil'
import {
RECEIVE_TODOS,
ADD_TODO,
REMOVE_TODO,
DELETE_DONE,
UPDATE_ALL_TODOS
} from './types'
export default {
readTodo({
commit
}) {
setTimeout(() => {
const todos = storageUtil.fetch()
// 提交commit 触发mutation 调用
commit(RECEIVE_TODOS, {
todos
})
}, 1000)
},
addTodo({
commit
}, todo) {
commit(ADD_TODO, {
todo
})
},
removeTodo({
commit
}, index) {
commit(REMOVE_TODO, {
index
})
},
deleteDone({
commit
}) {
commit(DELETE_DONE)
},
updateAllTodos({
commit
}, isCheck) {
commit(UPDATE_ALL_TODOS, {
isCheck
})
}
}
6.4.4. store/getters.js
// 含有多个计算属性对象
export default {
todos(state) {
return state.todos
},
totalSize(state) {
return state.todos.length
},
completeSize(state) {
return state.todos.reduce((preTotal, todo) => {
return preTotal + (todo.complete ? 1 : 0)
}, 0)
},
isAllComplete(state, getters) {
return getters.totalSize === getters.completeSize && getters.totalSize > 0
}
}
6.4.5. store/index.js
/*
vuex最核心的管理对象store
*/
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
Vue.use(Vuex)
const state = {
todos: []
}
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
6.4.6. components/app.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<todo-header></todo-header>
<todo-main></todo-main>
<todo-footer></todo-footer>
</div>
</div>
</template>
<script>
import todoHeader from "./todoHeader.vue";
import todoMain from "./todoMain.vue";
import todoFooter from "./todoFooter.vue";
import storageUtil from "../util/storageUtil";
export default {
created() {
// 模拟异步读取数据
this.$store.dispatch("readTodo");
},
components: {
todoHeader,
todoMain,
todoFooter
}
};
</script>
<style>
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
6.4.7. components/todoHeader.vue
<template>
<div class="todo-header">
<input
type="text"
placeholder="请输入你的任务名称,按回车键确认"
v-model="title"
@keyup.enter="addItem"
/>
</div>
</template>
<script type="text/ecmascript-6">
export default {
data() {
return {
title: null //组件内部使用的状态,不存在共享的问题-->不需要使用vuex管理它
};
},
methods: {
addItem() {
const title = this.title && this.title.trim();
if (title) {
const todo = {
title,
complete: false
};
this.$store.dispatch("addTodo", todo);
this.title = null;
}
}
}
};
</script>
<style>
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
6.4.8. components/todoMain.vue
<template>
<ul class="todo-main">
<todo-item
v-for="(todo, index) in todos"
:todo="todo"
:key="index"
:index="index"
></todo-item>
</ul>
</template>
<script type="text/ecmascript-6">
import todoItem from "./todoItem";
import storageUtil from "../util/storageUtil";
export default {
components: {
todoItem
},
computed: {
todos() {
return this.$store.getters.todos;
}
},
watch: {
todos: {
// 深度监视todos, 一旦有变化立即保存
handler: storageUtil.save,
deep: true
}
}
};
</script>
<style>
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
6.4.9. components/todoItem.vue
<template>
<li
:style="{ background: libg }"
@mouseenter="handleStyle(true)"
@mouseleave="handleStyle(false)"
>
<label>
<input type="checkbox" v-model="todo.complete" />
<span>{{ todo.title }}</span>
</label>
<button class="btn btn-danger" v-show="isShown" @click="deleteItem">
删除
</button>
</li>
</template>
<script type="text/ecmascript-6">
export default {
props: ["todo", "index"],
data() {
return {
isShown: false,
libg: "#fff"
};
},
methods: {
handleStyle(isEnter) {
if (isEnter) {
this.isShown = true;
this.libg = "#ddd";
} else {
this.isShown = false;
this.libg = "#fff";
}
},
deleteItem() {
const { todo, deleteTodo, index } = this;
if (window.confirm(`确定删除${todo.title}的评论吗?`)) {
// deleteTodo(index)
this.$store.dispatch("removeTodo", index);
}
}
}
};
</script>
<style>
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
6.4.10. components/todoFooter.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox" v-model="isAllDone" />
</label>
<span>
<span>已完成{{ completeSize }}</span> / 全部{{ totalSize }}
</span>
<button
class="btn btn-danger"
@click="deleteDone"
v-show="completeSize > 0"
>
清除已 完成任务
</button>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
methods: mapActions(["deleteDone"]),
computed: {
isAllDone: {
get() {
return this.$store.getters.isAllComplete;
},
set(value) {
//this.$emit('updateTodos', value)
this.$store.dispatch("updateAllTodos", value);
}
},
...mapGetters(["completeSize", "totalSize"])
}
};
/*
const arr1 = [1, 3, 5]
const arr2 = [4, ...arr1, 7]
const obj = {
a: 1,
b () {
}
}
const obj2 = {
c: 3,
...obj
}*/
</script>
<style>
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
6.4.11. util/storageUtil.js
// 使用localStorage存储数据的工具模块
// 1.函数 :1个功能
// 2.对象 :2个功能
// 需要向外暴露1个功能还是多个功能
var STORAGE_KEY = 'todos'
export default {
fetch() {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
},
save(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}
6.4.12. base.css
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
6.4.13. main.js
import Vue from 'vue'
import App from './components/App.vue'
import store from './store'
import './base.css'
new Vue({
el: '#app',
components: {App},
// template: '<App/>',
render: h => h(App),
store
})