什么是vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
每一个 Vuex 应用的核心就是 store(仓库)。先看一个简单例子:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
store.commit('increment')
console.log(store.state.count) // -> 1
核心概念
State
State 定义状态(变量)
在vue 组件中使用
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
}
})
new Vue({
el:'#app',
store,
computed:{
count(){
return store.state.count
}
}
})
mapState 辅助函数
import {mapState} from "vuex"
new Vue({
el:'#app',
store,
computed:mapState({
count:state => state.count,
})
})
当计算属性与 state 属性名称相同时,可以写成:
new Vue({
el:'#app',
store,
computed:mapState(['count'])
})
与局部计算属性混合使用
new Vue({
el:'#app',
store,
computed:{
local(){},
...mapState(['count'])
}
})
Getter
像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
new Vue({
el: '#app',
store,
computed: {
doneTodos(){
return store.getters.doneTodos
}
}
});
mapGetters 辅助函数
import {mapGetters} from "vuex"
new Vue({
el: '#app',
store,
computed: mapGetters(['doneTodos'])
// 混合使用
computed: {
local(){},
...mapGetters(['doneTodos'])
}
});
Mutation
Vuex 的 store 中的状态的唯一方法是提交 mutation。mutation
必须是同步函数。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state,payload) {
state.count += payload.num
}
}
})
store.commit('increment', {
num: 10
})
在组件中使用
使用 this.$store.commit('xxx')
提交 mutation, 或者使用 mapMutations
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', /* 将 `this.increment()` 映射为`this.$store.commit('increment')`*/
'incrementBy' /* 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`*/
]),
...mapMutations({
add: 'increment' /* 将 `this.add()` 映射为 `this.$store.commit('increment')`*/
})
}
}
使用常量替代 Mutation 事件类型
// mutation-types.js
export const INCREMENT = 'INCREMENT'
// store.js
import Vuex from 'vuex'
import { INCREMENT } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
[INCREMENT] (state,payload) {
state.count += payload.count
}
}
})
// app.vue
<template>
<div id="app">
<p>{{ count }}</p>
<button @click="increment({ count: 5 })">+5</button>
</div>
</template>
<script>
import { INCREMENT } from './mutation-types'
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations({
increment:INCREMENT
})
}
}
</script>
actions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
setTimeout(()=>{
context.commit('increment')
},1000)
}
}
})
在组件中使用
Action 通过 store.dispatch 方法触发 或者使用 mapActions
export default {
// ...
methods: {
increment(){
this.$store.dispatch('increment')
},
...mapActions(['increment']),
...mapActions({
add:'increment'
})
}
}
Module
当应用变得非常复杂时,store 对象变得相当臃肿。我们可以使用 module, 每个模块拥有自己的 state
、getter
、mutation
、action
。
const moduleA = {
state: () => ({
count:0
}),
mutations: {
incrementA(state){
state.count += 1
}
},
actions: { ... },
getters: {
doubleCount(state){
return state.count * 2
}
}
}
const moduleB = {
state: () => ({
count:1
}),
mutations: {
incrementB(state){
state.count += 2
}
},
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a.count // -> 0
store.commit('incrementA')
store.state.a.count // -> 1
store.getters.doubleCount // -> 2
store.state.b.count // -> 1
store.commit('incrementB')
store.state.b.count // -> 3
命名空间
如果把上面例子中 ncrementA
和 incrementB
改成 increment
,这就需要引入命名空间。默认情况下,模块内部的 getter
、mutation
、action
是注册在全局命名空间的。
添加namespaced:true
就可以使用带命名空间的模块。带命名空间模块访问全局内容,rootState 和 rootGetters 作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
在全局命名空间分发 action 或 提交 mutation , 将 {root:true}
作为第三参数传给 dispatch
或 commit
。
const moduleA = {
namespace:true,
state: () => ({
count:1
}),
mutations: {
increment(){ ... } // -> commit('a/increment')
},
actions: {
asyncIncrement({dispatch, commit, getters, rootGetters}){
getters.doubleCount // -> 'a/doubleCount' -> 2
rootGetters.doubleCount // -> 'doubleCount'
dispatch('someOtherAction') // -> 'a/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('increment') // -> 'a/increment'
commit('increment', null, { root: true }) // -> 'increment'
},
someOtherAction(ctx,payload){ ... }
},
getters:{
doubleCount(state, getters, rootState, rootGetters){
getters.someOtherGetter // -> 'a/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
return state.count * 2
},
someOtherGetter: state => { ... }
}
}
const moduleB = {
namespace:true,
state: () => ({
count:0
}),
mutations: {
increment(){ ... } // -> commit('b/increment')
},
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
},
getters:{
someOtherGetter: state => { ... }
}
mutations: {
increment(){ ... } // -> commit('increment')
},
actions:{
someOtherAction(ctx,payload){ ... }
}
})
带命名空间的模块注册全局的action
注册全局 action ,需添加 root:true
,并将action的定义放在 函数handler
中。
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
组件中使用带命名空间的模块
在组件中使用带命名空间的模块,mapState
, mapGetters
, mapActions
和 mapMutations
写法比较繁琐。解决方法,将模块的空间名称字符串作为第一个参数传入。
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
// 简化
computed: {
...mapState('some.nested.module',{
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module',[
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
也可以通过createNamespacedHelpers
创建基于某个命名空间辅助函数,返回一个对象。
import {createNamespacedHelpers} from "vuex"
const {mapState,mapActions} from = createNamespacedHelpers('some/nested/module')
export default{
computed:{
...mapState({
a:state=>state.a,
b:state=>state.b
})
},
methods:{
...mapActions({
'foo',
'bar'
})
}
}
实现 todoList
参考vue 官网todos 实现
store 实现
// store.js
import Vue from "vue";
import Vuex from "vuex";
import todoStorage from "./lib/todoStorage";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
todos: todoStorage.fetch(),
newTodo: "",
editedTodo: null,
visibility: "all",
loading: false
},
getters: {
activeTodo(state) {
return state.todos.filter((todo) => !todo.completed);
},
completedTodo(state) {
return state.todos.filter((todo) => todo.completed);
},
findTodo(state, getters) {
if (state.visibility === "all") {
return state.todos;
} else if (state.visibility === "active") {
return getters.activeTodo;
} else if (state.visibility === "completed") {
return getters.completedTodo;
}
},
allDone(state, getters) {
return getters.activeTodo.length === 0;
},
remaining(state, getters) {
return getters.activeTodo.length;
}
},
mutations: {
addTodo(state, payload) {
let uid = state.todos.length;
state.todos.push({
id: uid++,
title: payload,
completed: false
});
state.newTodo = "";
todoStorage.save(state.todos);
},
removeTodo(state, payload) {
state.todos.splice(state.todos.indexOf(payload), 1);
todoStorage.save(state.todos);
},
updateTodo(state, payload) {
let index = state.todos.findIndex((todo) => todo.id === payload.id);
if (payload.completed !== undefined) {
state.todos[index].completed = payload.completed;
} else if (payload.title !== undefined) {
state.todos[index].title = payload.title;
}
todoStorage.save(state.todos);
},
removeCompleted(state) {
state.todos = state.todos.filter((todo) => !todo.completed);
todoStorage.save(state.todos);
},
updateNewTodo(state, message) {
state.newTodo = message;
},
updateAllDone(state, message) {
state.allDone = message;
state.todos.forEach((todo) => (todo.completed = message));
},
setLoading(state, payload) {
state.loading = payload;
},
setVisibility(state, payload) {
state.visibility = payload;
},
setEditedTodo(state, message) {
state.editedTodo = message;
}
},
actions: {
asyncAddTodo({ commit }, payload) {
return new Promise((resolve, reject) => {
commit("setLoading", true);
setTimeout(() => {
commit("setLoading", false);
commit("addTodo", payload);
resolve();
}, 1000);
});
},
asyncRemoveTodo({ commit }, payload) {
return new Promise((resolve, reject) => {
commit("setLoading", true);
setTimeout(() => {
commit("setLoading", false);
commit("removeTodo", payload);
resolve();
}, 1000);
});
},
asyncUpdateTodo({ commit }, payload) {
return new Promise((resolve, reject) => {
commit("setLoading", true);
setTimeout(() => {
commit("setLoading", false);
commit("updateTodo", payload);
}, 1000);
});
},
asyncFindTodo({ commit }, payload) {
return new Promise((resolve, reject) => {
commit("setLoading", true);
setTimeout(() => {
commit("setLoading", false);
commit("setVisibility", payload);
}, 300);
});
},
asyncRemoveCompleted({ commit }) {
return new Promise((resolve, reject) => {
commit("setLoading", true);
setTimeout(() => {
commit("setLoading", false);
commit("removeCompleted");
}, 1000);
});
}
}
});
export default store;
todoStorage
插件
// lib/todoStorage.js
var STORAGE_KEY = "todolist";
var todoStorage = {
fetch: function () {
var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
todos.forEach(function (todo, index) {
todo.id = index;
});
return todos;
},
save: function (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}
};
export default todoStorage;
AllDone
组件
// AllDone
<template>
<div>
<input
id="toggle-all"
class="toggle-all"
type="checkbox"
v-model="allDone"
/>
<label for="toggle-all"></label>
</div>
</template>
<script>
export default {
computed: {
allDone: {
get() {
return this.$store.getters.allDone;
},
set(value) {
this.$store.commit("updateAllDone", value);
},
},
},
};
</script>
Header 组件
// Header
<template>
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
autofocus
autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="asyncAddTodo($event.target.value)"
/>
</header>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "header",
computed: {
newTodo: {
get() {
return this.$store.state.newTodo;
},
set(value) {
this.$store.commit("updateNewTodo", value);
},
},
},
methods: {
...mapActions(["asyncAddTodo"]),
},
};
</script>
TodoList 组件
// TodoList
<template>
<section class="main" v-show="todos.length">
<AllDone />
<ul class="todo-list">
<li
v-for="todo in findTodo"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }"
>
<TodoItem
:todo="todo"
:completed.sync="todo.completed"
:title.sync="todo.title"
/>
</li>
</ul>
</section>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import AllDone from "./AllDone";
import TodoItem from "./TodoItem";
export default {
name: "todoList",
components: {
AllDone,
TodoItem,
},
computed: {
...mapState(["todos", "editedTodo"]),
...mapGetters(["findTodo"]),
},
};
</script>
TodoItem组件
// TodoItem
<template>
<div>
<div class="view">
<input
class="toggle"
:class="{ checked: completed }"
type="checkbox"
:value="completed"
@change="changeCompleted"
/>
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input
class="edit"
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
/>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
props: ["todo", "completed", "title"],
data() {
return {
beforeEditCache: "",
};
},
computed: {
...mapState(["editedTodo"]),
},
methods: {
changeCompleted() {
this.$emit("update:completed", !this.completed);
},
editTodo(todo) {
this.beforeEditCache = todo.title;
this.$store.commit("setEditedTodo", todo);
},
doneEdit(todo) {
if (!this.editedTodo) {
return;
}
this.$store.commit("setEditedTodo", null);
let title = todo.title.trim();
if (!title) {
this.removeTodo(todo);
}
},
cancelEdit(todo) {
this.$store.commit("setEditedTodo", null);
this.$emit("update:title", this.beforeEditCache);
},
...mapActions({
removeTodo: "asyncRemoveTodo",
}),
},
directives: {
"todo-focus": function (el, binding) {
if (binding.value) {
el.focus();
}
},
},
};
</script>
Footer 组件
// Footer
<template>
<footer class="footer" v-show="todos.length">
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li v-for="item in types" :key="item.id">
<a
href="javascript:;"
:class="{ selected: visibility == item.type }"
@click="asyncFindTodo(item.type)"
>{{ item.type }}</a
>
</li>
</ul>
<button
class="clear-completed"
@click="asyncRemoveCompleted"
v-show="todos.length > remaining"
>
Clear completed
</button>
</footer>
</template>
<script>
import { mapState, mapActions, mapGetters } from "vuex";
export default {
data() {
return {
types: [
{
id: 1,
type: "all",
},
{
id: 2,
type: "active",
},
{
id: 3,
type: "completed",
},
],
};
},
computed: {
...mapState(["todos", "visibility"]),
...mapGetters(["activeTodo", "remaining"]),
},
filters: {
pluralize: function (n) {
return n === 1 ? "item" : "items";
},
},
methods: {
...mapActions(["asyncFindTodo", "asyncRemoveCompleted"]),
},
};
</script>
网友评论