# [vuex状态管理模式]

每一个Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

# 安装vuex

npm install vuex --save 

理解

vuex理解 是状态管理模式。它采用集中式存储管理应用的所有的状态,并以相应的规则保证状态以一种可预测的方式发生变化。可以理解为是一个全局变量

  • state 数据中心
  • mutations 操作数据
  • actions 什么时候触发操作,执行动作,改变数据

# 使用vuex

# 创建一个store.js用来存取数据

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  // state :localStorage.getItem('state') ? JSON.parse(localStorage.getItem('state')) 刷新后保留数据的办法
  state: {
    count: 0,
	todos: [
	      { id: 1, text: '...', done: true },
	      { id: 2, text: '...', done: false }
	]
  },
  getters: {
    // getter可以认为是store的计算属性
    // getter正常接受两个参数,分别表示 state, getters(在使用module时,还会有根store的state和getters)
    doubleCount: (state, getters) => {
      return state.count * 2
    },
	doneTodos (state) {
	      return state.todos.filter(todo => todo.done)
	},
	//Getter 也可以接受其他 getter 作为第二个参数:
	doneTodosCount (state, getters) {
	  return getters.doneTodos.length
	}
	
  },
  mutations: {
    increment (state) {
      state.count++
    },
    addForNum (state, payload) {
      state.count += Number(payload.num)
    },
    asyncAdd (state) {
      state.count += 10
    },
	login(state){
		
	}
  },
  actions: {
    // 异步操作需要放在actions中执行,然后使用commit交给对应的mutations修改state的值
    asyncAddAction (context) {
		//这里的context和我们使用的$store拥有相同的对象和方法
      setTimeout (function () {
        context.commit('asyncAdd')
      }, 500)
    },
	login({commit}, username) {
		return new Promise((resolve, reject) => {
			setTimeout(() => {
				if (username === 'admin') {
					commit('login')
					resolve()
				} else {
					reject()
				}
			}, 1000);
		})
	}
  }
})

export default store

  • Action 通常是异步的,那么如何知道 action 什么时候结束呢?可以通过让action返回Promise,在Promise的then中来处理完成后的操作;
  • dispatch派发请求异步接受信息
this.$store.dispatch('login', 'admin').then(() => {
	  this.$router.push(this.$route.query.redirect)
	}).catch(() => {
	  alert('用户名或密码错误')
  })

# computed与vuex数据的结合运用

computed:{ 
    category: { 
     get() { 
     return this.$store.state.category 
     }, 
     set (value) { 
     console.log("Value of category changed") 
     this.store.commit("SET_CAT", value) 
     } 
    } 
} 
import { mapGetters } from 'vuex'

computed: {
    ...mapGetters(['name'])
}

# 在main.js里引用

import store from './store'
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

# 在vue文件中使用

<template>
  <div>
    <h2>这是计数器页面</h2>
    <div>
      <div>计数:{{count}}</div>
      <div>双倍计数:{{doubleCount}}</div>
      <br/>
      <button @click="increment">+1</button>
      <div>
        增加值 <input type="text" v-model="num" />
        <button @click="addForNum">=</button>
      </div>
      <div>
        <button @click="asyncAdd">异步加10</button>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'index',
  data () {
    return {
      num: 0
    }
  },
  computed: {
    count () {
      return this.store.state.count
    },
    doubleCount () {
      return this.store.getters.doubleCount
    }
  },
  methods: {
    // 自增1
    increment () {
		//this.$store.state.number++ //不推荐,严格模式无效
      this.store.commit('increment')
    },
    // 加指定的值
    addForNum () {
      this.store.commit('addForNum', {num: this.num})
    },
    // 异步加1
    asyncAdd () {
      this.$store.dispatch('asyncAddAction')
    }
  }
}
</script>

# vuex的辅助函数

可以使用map辅助函数使代码更精简

  • store文件中的内容不需要变动,只要在组件中使用map辅助函数来替代全局的this.$sotre下的属性和方法即可。
  • 注意辅助函数mapGetters的对象写法,只有重命名,没有函数形式
<template>
  <div>
    <h2>这是计数器页面</h2>
    <div>
      <div>计数:{{count}}  -  {{countAlias}}</div>
      <div>双倍计数:{{doubleCount}}  -  {{doubleCountAlias}}</div>
      <br/>
      <button @click="increment">+1</button>
      <div>
        增加值 <input type="text" v-model="num" />
        <button @click="incrementBy(num)">=</button>
      </div>
      <div>
        <button @click="asyncAdd">异步加10</button>
      </div>
    </div>
  </div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'index',
  data () {
    return {
      num: 0,
      localCount: 10
    }
  },
  // computed:mapState(['a','b']),
  //如果computed只获取store中的数据且不需要转换,那么可以直接使用mapState,不需要解构
  //mapState本身返回的就是个对象
  computed: {

    /*
    * ------------------------------------------------------------
    * mapState
    */

    // 【传统形式】
    // count () {
    //   return this.store.state.count
    // },

    // 【对象形式】
    ...mapState({
      // 箭头函数可使代码更简练
      count: state => state.count,

      // 传字符串参数 'count' 等同于 `state => state.count`
      // 使用这种方式可以更简洁地为state生成的计算属性定义一个别名
      countAlias: 'count',

      // 为了能够使用 `this` 获取局部状态,必须使用常规函数
      countPlusLocalState (state) {
        return state.count + this.localCount
      }
    }),

    // 【数组形式】
    // 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
    ...mapState([
      // 映射 this.count 为 store.state.count
      'count'
    ]),



    /*
    * ------------------------------------------------------------
    * mapGetters
    */

    // 【传统形式】
    // doubleCount () {
    //   return this.store.getters.doubleCount
    // }

    // 【对象形式】
    // 如果你想将一个 getter 属性另取一个名字,使用对象形式
    ...mapGetters({
      // 把 `this.doubleCountAlias` 映射为 `this.store.getters.doubleCount`
      doubleCountAlias: 'doubleCount'
    }),

    // 【数组形式】
    ...mapGetters(['doubleCount'])
  },
  methods: {

    /*
    * ------------------------------------------------------------
    * mapMutations
    */

    // 【传统形式】
    // 自增1
    // increment () {
    //   this.store.commit('increment')
    // },
    // // 加指定的值
    // incrementBy (amount) {
    //   this.store.commit('incrementBy', amount)
    // },

    // 【数组形式】
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.store.commit('incrementBy', amount)`
    ]),

    // 【对象形式】
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.store.commit('increment')`
    }),

    // // 异步加1
    // asyncAdd () {
    //   this.store.dispatch('asyncAddAction')
    // }

    /*
    * ------------------------------------------------------------
    * mapActions
    */

   // 【数组形式】
    ...mapActions([
      'asyncAddAction', // 将 `this.asyncAddAction()` 映射为 `this.store.dispatch('asyncAddAction')`
    ]),

    // 【对象形式】
    ...mapActions({
      asyncAdd: 'asyncAddAction' // 将 `this.asyncAdd()` 映射为 `this.$store.dispatch('asyncAddAction')`
    })
  }
}

</script>

因为辅助函数返回的本来就是对象,所以要用一次展示运算符,除非是直接如computed:mapState(['xxx'])这种外层没加包裹形式

<button @click="login" v-if="!isLogin">登录</button>

当需要映射的值时模块化的值,如是user模块,可以用以下两种方式来写

import { mapState } from 'vuex'
computed: {
...mapState('user', ['isLogin']) //推荐,在template中可以直接使用isLogin
//...mapState(['user/isLogin']) // 不推荐,用的时候不好写
}

import { mapActions } from 'vuex'
methods: {
login() {
	this['user/login']('admin').then(...)
},
...mapActions(['user/login', 'user/logout'])
//...mapActions(user,['login'])//这种写法也可以,但是如果methods中还有别的也叫login的方法就冲突了!!!
},

# vuex modules模块化设计

新建modules文件夹在store文件夹内,选项namesapced保证命名不冲突

+ store 
	+ modules
		- a.js 
		- b.js
		- index.js
//a.js b.js与之结构类似
const state={
	count:1
}

const mutations={
	add(state){
		state.count++
	}
}
const actions={
	add:({commit})=>{
		commit("add")
	}
}

export default{
	namespaced:true,
	state,
	mutations,
	actions
}
//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import count from './a'
import number from './b'

Vue.use(Vuex)
export default new Vuex.Store({
	modules:{
		count,
		number
	}
})
//项目main.js
import store from './store/modules/index.js'
<template>
  <div class="home">
		<div>{{$store.state.count.count}}</div>
		<button @click="add">+</button>
		<button @click="miss">-</button>
  </div>
</template>
<script>
import {mapMutations} from 'vuex'

export default {
  name: 'Home',
  data(){
	  return{
		  a:""
	  }
  },
  methods:{
	  add(){
		  this.$store.state.count.count++;	 
	  },
	  miss(){
		   this.$store.commit('count/add')//两种操作写法,效果一致
       //对象写法风格
		   this.$store.commit({
			   type:'count/add'
		   })
	  }
	 
  }
}
</script>

提示

默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的,也就是说,子模块中有mutation/action/getters中有某个属性或者方法funa,根模块也有,那么比如操作commit('funa'),都会触发!

  • 默认情况下Getter 同样也默认注册在全局命名空间;(this.$store.state.a.b(正确)=>a模块下的state中的b,this.$store.getters.a.geta(错误),不需要.getters)
  • 如果希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:
  • 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名;
  • $store.getters["home/doubleHomeCounter"]:home模块下的doubleHomeCounter
<template>
    <div>
      {{this.$store.state.count}}<br/>
      {{count}}<br/>
      {{this.$store.getters.changeCount}}<br/>
      <el-button type="primary" @click="add">主要按钮</el-button>
    </div>
</template>
 
<script>
import {mapState} from 'vuex'
export default {
  name: 'home',
  computed: {
    ...mapState([
      'count'
    ])
  },
  methods: {
    add () {
      this.$store.dispatch('addFun', 10) // actions    this.$store.commit('add',10) //mutations
    }
  },
  mounted: {
  }
}
</script>
 
<style scoped>
 
</style>
import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 1
  },
  getters: {
    changeCount: state => {
      return state.count + 1
    }
  },
  mutations: {
    add (state, n) {
      state.count = state.count + n
    }
  },
  actions: {
    addFun (context, n) {
      context.commit('add', n)
    }
  }
})
export default store

总结

  • modules模块使用需要在每个js里加上命名空间选项namespaced:true
  • indexjs注意写法
  • this.$store.commit('count/add')

# createNamespacedHelpers

可以使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数。

<template>
  <div>
    <hr>
    <h2>{{ homeCounter }}</h2>
    <h2>{{ doubleHomeCounter }}</h2>
    <!-- <h2>{{ doubleRootCounter }}</h2> -->
    <button @click="increment">home+1</button>
    <button @click="incrementAction">home+1</button>
    <hr>
  </div>
</template>

<script>
  import { createNamespacedHelpers, mapState, mapGetters, mapMutations, mapActions } from "vuex";

  import { useState, useGetters } from '../hooks/index'

  const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home")

  export default {
    computed: {
      // 1.写法一: 
      // ...mapState({
      //   homeCounter: state => state.home.homeCounter
      // }),
      // ...mapGetters({
      //   doubleHomeCounter: "home/doubleHomeCounter"
      // })

      // 2.写法二:
      ...mapState("home", ["homeCounter"]),
      ...mapGetters("home", ["doubleHomeCounter"])

      // 3.写法三:直接通过createNamespacedHelpers绑定对应的命名空间,可以不需要像写法二那样去处理
      ...mapState(["homeCounter"]),
      ...mapGetters(["doubleHomeCounter"])
    },
    methods: {
      // 1.写法一: 
      // ...mapMutations({
      //   increment: "home/increment"
      // }),
      // ...mapActions({
      //   incrementAction: "home/incrementAction"
      // }),

      // 2.写法二
      ...mapMutations("home", ["increment"]),
      ...mapActions("home", ["incrementAction"]),
      
      // 3.写法三:
      ...mapMutations(["increment"]),
      ...mapActions(["incrementAction"]),
    },

    setup() {
      // {homeCounter: function}
      const state = useState(["rootCounter"])
      const rootGetters = useGetters(["doubleRootCounter"])
      const getters = useGetters("home", ["doubleHomeCounter"])

      const mutations = mapMutations(["increment"])
      const actions = mapActions(["incrementAction"])

      return {
        ...state,
        ...getters,
        ...rootGetters
        ...mutations,
        ...actions
      }
    }
  }
</script>
  • useState.js
import { mapState, createNamespacedHelpers } from 'vuex'
import { useMapper } from './useMapper'

export function useState(moduleName, mapper) {
  let mapperFn = mapState
  if (typeof moduleName === 'string' && moduleName.length > 0) {
    mapperFn = createNamespacedHelpers(moduleName).mapState
  } else {
    mapper = moduleName
  }

  return useMapper(mapper, mapperFn)
}
  • useMapper
import { computed } from 'vue'
import { useStore } from 'vuex'

export function useMapper(mapper, mapFn) {
  // 拿到store独享
  const store = useStore()

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapFn(mapper)

  // 对数据进行转换
  const storeState = {}
  Object.keys(storeStateFns).forEach(fnKey => {
    const fn = storeStateFns[fnKey].bind({$store: store})
    storeState[fnKey] = computed(fn)
  })

  return storeState
}

# require.context 借助webpack来读modules文件下文件,避免一个个手动引入

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'

Vue.use(Vuex)

// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)

// you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  // set './app.js' => 'app'
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = modulesFiles(modulePath)
  modules[moduleName] = value.default
  return modules
}, {})

console.log( modulesFiles,modules)

const store = new Vuex.Store({
  modules,
  getters
})

export default store

require.context(directory, useSubdirectories, regExp)
//directory: 要查找的文件路径
//useSubdirectories: 是否查找子目录
// regExp: 要匹配文件的正则
代码很简单,require.context执行后,返回一个方法webpackContext,这个方法又返回一个__webpack_require__,
这个__webpack_require__就相当于require或者import。同时webpackContext还有二个静态方法keys与resolve,一个id属性。

keys: 返回匹配成功模块的名字组成的数组
resolve: 接受一个参数request,request为test文件夹下面匹配文件的相对路径,返回这个匹配文件相对于整个工程的相对路径
id: 执行环境的id,返回的是一个字符串,主要用在module.hot.accept,应该是热加载

# vuex严格模式

无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。开启严格模式 strict: true

const store = new Vuex.Store({
	// ...
	strict: true
})

# vuex插件写法

Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

const myPlugin = store => {
	// 当 store 初始化后调用
}

注册插件:

import myPlugin from "xxx"
const store = new Vuex.Store({
	// ...
	plugins: [myPlugin]
})

范例:实现登录状态持久化,store/plugins/persist.js

  • store.subscribe插件中的方法,可以监听到对应的mutation state变更信息,同步操作
export default store => {
	// 初始化时从localStorage获取数据
	if(localStorage) {
		const user = JSON.parse(localStorage.getItem('user'))
	if (user) {
		store.commit('user/login')
		store.commit('user/setUsername', user.username)
	}
	}
	// 用户状态发生变化时缓存之
	store.subscribe((mutation, state) => {
	if (mutation.type.startsWith('user/')) {
	localStorage.setItem('user', JSON.stringify(state.user))
	} else if (mutation.type === 'user/logout') {
	localStorage.removeItem('user')
	}
	})
}
  • 借助vuex-createPersistedState插件实现缓存数据。
import createPersistedState from 'vuex-createPersistedState'
const PERSIST_PATHS = ['user']
const store = new Vuex.Store({
  state: {},
  modules: {
    app,
    settings,
    user,
    permission,
    tagsView
  },
  getters,
  plugins: [createPersistedState({
    storage: window.sessionStorage,
    paths: PERSIST_PATHS
  })]
})

# vuex手写

  • 实现插件 -实现Store类
    • 维持1个响应式状态state
    • 实现commit()
    • 实现dispatch()
    • getters
    • 挂载$store
let Vue
class Store{
	constructor(option = {}) {
	  this.$option = option 
	  console.log(option)
	 
	  this._wrapgetters =option.getters
	  const computed ={}
	  this.getters ={}
	  const store =this
	  Object.keys(this._wrapgetters).forEach(key=>{
		  //获取用户定义的getters
		  console.log(store,store._wrapgetters)
		  const fn = store._wrapgetters[key]
		  //转化为computed可以使用无参形式
		  computed[key] =function(){
			  return fn(store.state)
		  }
		  Object.defineProperty(store.getters,key,{
			  get:()=>store.vm[key]
		  })
	  })
	  console.log(computed)
	  this.vm = new Vue({
		  data:{
			  state:option.state
		  },
		  computed
	  })
	 
	  this.mutations =option.mutations
	  this.actions =option.actions
	  this.commit =this.commit.bind(this)
	  this.dispatch =this.dispatch.bind(this)
	  
	}
	get state(){
			 return this.vm.state
	}
	set state(v){
			 console.error("warning",'do not to change state')
	}
	commit(v,t){
		console.log(this)
		// 需要在constructor中给commit方法绑定好this的指向,否则调用会undefined
		// console.log(this.mutations[v])
		// console.log(this.vm.state,t)
		if(v){
			this.mutations[v](this.vm.state,t)
		}
	}
	dispatch(v){
		// console.log(v)
		console.log(this.actions[v])
		console.log(this)
		if(v){
			this.actions[v](this,v)
		}
	}
}
function install(vue){
  Vue = vue
  Vue.mixin({
	  beforeCreate(){
		  // console.log('beforeCreate')
		 if(this.$options.store){
		 		  Vue.prototype.$store = this.$options.store
		 } 
	  } 
  })

}
export default {Store,install}

# vue3中使用vuex

  • vuex创建的写法上与之前的也有所不同
  • getters类似computed,他返回的是该函数的计算结果,当然也可以返回函数,那么就可以再传递一个可用参数实现复用
import { createStore } from "vuex"
import home from './modules/home'
import user from './modules/user'

const store = createStore({
  state() {
    return {
      rootCounter: 100
    }
  },
  getters: {
    doubleRootCounter(state) {
      return state.rootCounter * 2
    },
    totalPriceCountGreaterN(state, getters) {
      return function(n) {
        let totalPrice = 0
        for (const book of state.books) {
          if (book.count > n) {
            totalPrice += book.count * book.price
          }
        }
        return totalPrice * getters.currentDiscount
      }
    }
  },
  mutations: {
    increment(state) {
      state.rootCounter++
    }
  },
  modules: {
    home,
    user
  }
});

export default store;

# 在setup辅助函数的使用

在setup中,computed里面只能放函数形式,不能类似options中的可以以对象的形式放多个computed的属性,要么一个state使用一个computed,要么通过遍历的方式,利用hooks去处理。mapState返回的其实每个属性就是函数。同样可以对mapGetters进行相同的封装。

import { computed } from 'vue'
import { mapState, useStore } from 'vuex'

export function useState(mapper) {
  // 拿到store独享
  const store = useStore()

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapState(mapper)

  // 对数据进行转换
  const storeState = {}
  Object.keys(storeStateFns).forEach(fnKey => {
    const fn = storeStateFns[fnKey].bind({$store: store})
    storeState[fnKey] = computed(fn)
  })

  return storeState
}
<template>
  <div>
    <h2>Home:{{ $store.state.counter }}</h2>
    <hr>
    <h2>{{counter}}</h2>
    <h2>{{name}}</h2>
    <h2>{{sCounter}}</h2>
    <h2>{{sName}}</h2>
    <hr>
  </div>
</template>

<script>
  // import { computed } from 'vue'
  import { useState } from '../hooks/useState'
  export default {
    setup() {
      // const sCounter = computed(() => store.state.counter)//默认写法
      const storeState = useState(["counter", "name"])
      const storeState2 = useState({
        sCounter: state => state.counter,
        sName: state => state.name
      })

      return {
        ...storeState,
        ...storeState2
      }
    }
  }
</script>

# mutation在setup中使用

在setup中使用mutation比较简单,不需要像state和getters那样麻烦

<template>
  <div>
    <h2>当前计数: {{ $store.state.counter }}</h2>
    <hr>
      <button @click="increment">+1</button>
      <button @click="add">+1</button>
      <button @click="decrement">-1</button>
      <button @click="increment_n({n: 10})">+10</button>
    <hr>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'

  import { INCREMENT_N } from '../store/mutation-types'

  export default {
    methods: {
      ...mapMutations(["increment", "decrement", INCREMENT_N]),
      ...mapMutations({
        add: "increment"
      })
    },
    setup() {
      const storeMutations = mapMutations(["increment", "decrement", INCREMENT_N])
      return {
        ...storeMutations
      }
    }
  }
</script>

<style scoped>

</style>

# pinia

vuex的使用 (opens new window) vuex持久化存储 (opens new window)

最后更新: 9/5/2022, 9:53:26 AM