一、初始化Vuex
Vuex是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
如果一份数据需要在多个组件中使用,组件间传值又比较复杂,就可以使用vuex托管数据。
1、安装
npm install vuex --save
2、导入
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
3、创建状态管理对象 store
state选项:定义状态(状态就是数据)。
mutations选项:定义修改状态的方法(注意:这里面只能定义同步方法)。
export default new Vuex.Store({
// 定义全局状态(状态就是数据)
state:{
car:{
name:'奔驰',
price:'40W'
}
},
// 定义修改状态的方法
mutations:{
//该方法,修改汽车信息
updateCar(state,val){
state.car = val
}
}
})
4、注册给Vue
// 导入当前项目中的全局状态管理对象
import store from './store'
new Vue({
// 在vue实例中使用全局状态管理对象
store,
render: h => h(App)
}).$mount('#app')
5、简单使用
$store:返回的是当前项目中的全局状态对象。
commit()方法:用于执行指定的mutations里面的方法。
(1)获取数据
在组件中,直接通过$store.state就可以获取到全局状态对象管理的状态数据,直接渲染到页面。
<div>车辆名称:{{ $store.state.car.name }}</div>
<div>车辆价格:{{ $store.state.car.price }}</div>
(2)修改数据
<div>车辆名称:{{ $store.state.car.name }}</div>
<div>车辆价格:{{ $store.state.car.price }}</div>
<button @click="updateCar">修改汽车信息</button>
methods: {
updateCar() {
this.$store.commit("updateCar", { name: "奔驰", price: "60W" });
}
}
二、核心概念
1、state
state选项:定义状态(状态就是数据)。
state: {
name:'张三'
}
通过$store.state.数据名使用。
<div>姓名:{{ $store.state.name }}</div>
2、getters
getters选项:定义计算属性。方法的参数是状态对象。
getters:{
// 方法的第一个参数是全局状态
nameInfo(state){
return `我的名字叫${state.name}`
}
}
通过$store.getters.属性名使用计算属性。
<div>{{ $store.getters.nameInfo }}</div>
3、mutations
mutations选项:定义修改状态的方法(注意:这里的方法一般都是同步方法)。方法的第一个参数是状态对象,第二个参数是新值。
mutations:{
// 修改姓名
updateName(state,val){
state.name = val
}
},
通过commit()方法,调用mutations里面的方法。
this.$store.commit("updateName", '李四');
4、actions
actions选项:定义操作状态的方法(这里的方法可以定义异步方法)。
注意:actions里的方法最好不要直接操作state状态,而是通过调用mutations里面的方法去修改状态。所以,actions直接操作的是mutations。
state: {
carAddress:'意大利'
},
mutations:{
//修改汽车的产地
updateCarAddress(state,val){
state.carAddress = val
}
},
actions:{
//修改汽车的产地
//方法的第一个参数是全局状态管理对象,第二个参数是具体的值
updateCarAddress(store,val){
axios.get(val).then(({data})=>{
// 方式一:这里可以直接修改状态
// store.state.carAddress = data.address
// 方式二:通过调用mutations里面的方法修改状态
store.commit('updateCarAddress',data.address)
})
}
}
通过dispatch()方法,调用actions里面定义的方法。
this.$store.dispatch('updateCarAddress','data/address.json')
5、modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
(1)定义模块
namespaced属性:默认情况下,action、mutation 和 getter 是注册在全局命名空间的。通过设置namespaced属性为true,将action、mutation 和 getter全部注册到私有命名空间中。
export default {
namespaced:true,
// 状态
state:{
planeName:'空客404',
planePrice:'10Y',
planeAddress:'中国'
},
// 计算属性
getters:{
planeInfo(state){
return `飞机名称:${state.planeName},飞机价格:${state.planePrice},飞机产地:${state.planeAddress}`
}
},
// 同步方法
mutations:{
updatePlaneName(state,val){
state.planeName = val
},
updatePlanePrice(state,val){
state.planePrice = val
}
},
// 异步方法
actions:{
updatePlanePrice(store,val){
setTimeout(() => {
store.commit('updatePlanePrice',val)
}, 500);
}
}
}
(2)在全局状态管理对象中导入模块
// 导入飞机模块
import plane from './modules/plane.js'
// 创建一个全局状态管理对象并导出
export default new Vuex.Store({
// 模块
modules:{
// 飞机模块
plane
}
})
(3)使用模块
① 获取模块中的state状态
要从私有模块中获取数据,方式是:$store.state.模块名称.模块的数据 。
<div>飞机名称:{{ planeName }}</div>
planeName() {
return this.$store.state.plane.planeName;
}
② 获取模块中的getters计算属性
要从私有模块中获取计算属性,方式是:$store.getters['模块名/计算属性']。
<div>{{planeInfo}}</div>
planeInfo(){
return this.$store.getters['plane/planeInfo']
}
③ 调用模块中的mutations定义的方法
调用私有模块里面的mutations定义的方法,方式是:$store.commit('模块名/方法名',新值)。
<button @click="updatePlaneName">修改飞机名称</button>
updatePlaneName(){
this.$store.commit('plane/updatePlaneName','波音747')
}
④ 调用模块中的actions定义的方法
调用私有模块里面的actions定义的方法,方式是:$store.dispatch('模块名/方法名',新值)。
<button @click="updatePlanePrice">修改飞机价格</button>
updatePlanePrice(){
this.$store.dispatch('plane/updatePlanePrice','20Y')
}
三、Vuex使用
1、计算属性中转
直接在模板中使用全局状态管理数据,表达式会写的很长。所以可以使用计算属性。
// getters选项定义计算属性
getters:{
carInfo(state){
return `汽车名称:${state.carName},汽车价格:${state.carPrice},汽车产地:${state.carAddress}`
}
}
<!-- 直接在模板中使用全局状态里面的计算属性 -->
<div>{{$store.getters.carInfo}}</div>
<div>{{carInfo}}</div>
//计算属性
computed:{
// 汽车信息
carInfo(){
// 返回全局状态管理里面的计算属性
return this.$store.getters.carInfo
}
}
2、映射函数
通过映射函数mapState、mapGetters、mapActions、mapMutations,可以将vuex.store中的属性映射到vue实例身上,这样在vue实例中就能访问vuex.store中的属性了,便于操作vuex.store。
(1)导入映射函数
// 从vuex中,导入映射函数
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
(2)使用映射函数生成计算属性
如果vuex里面state的数据名称 跟 页面中的计算属性名称相同,就可以使用mapState映射函数,自动生成页面中的计算属性。
如果vuex里面getters的数据名称 跟 页面中的计算属性名称相同,就可以使用mapGetters映射函数,自动生成页面中的计算属性。
注意:如果要映射模块里面的state/getters,函数的第一个参数设置为模块的名称。
<div>汽车名称:{{ carName }}</div>
<div>汽车价格:{{ carPrice }}</div>
<div>汽车产地:{{ carAddress }}</div>
<div>{{ carInfo }}</div>
<hr/>
<div>飞机名称:{{ planeName }}</div>
<div>飞机价格:{{ planePrice }}</div>
<div>飞机产地:{{ planeAddress }}</div>
<div>{{ planeInfo }}</div>
computed: {
// mapState映射state
...mapState(["carName", "carPrice", "carAddress"]),
// mapGetters映射getters
...mapGetters(["carInfo"]),
// 映射私有模块里面的数据
...mapState('plane',['planeName','planePrice','planeAddress']),
...mapGetters('plane',['planeInfo'])
}
(3)使用映射函数生成方法
如果定义的方法名跟全局管理对象中mutations里面的方法名相同,并且定义的方法会带有一个参数,通过参数传递数据。满足该规则,就可以使用mapMutations映射函数生成方法。
如果定义的方法名跟全局管理对象中actions里面的方法名相同,并且定义的方法会带有一个参数,通过参数传递数据。满足该规则,就可以使用mapActions映射函数生成方法。
注意:如果要映射私有模块中mutations/actions里面的方法,函数的第一个参数设置为模块的名称。
<button @click="updateCarName('宾利')">修改汽车名称</button>
<button @click="updateCarPrice('300W')">修改汽车价格</button>
<button @click="updateCarAddress('data/address.json')">修改汽车产地</button>
<hr/>
<button @click="updatePlaneName('波音747')">修改飞机名称</button>
<button @click="updatePlanePrice('20Y')">修改飞机价格</button>
methods: {
// 映射全局管理对象中mutations里面的方法
...mapMutations(["updateCarName", "updateCarPrice"]),
// 映射全局管理对象中actions里面的方法
...mapActions(["updateCarAddress"]),
// 映射私有模块里面的方法
...mapMutations('plane',['updatePlaneName']),
...mapActions('plane',['updatePlanePrice'])
}
3、购物车模块
import axios from "axios";
export default {
namespaced: true,
state: {
//商品数组
goodses: [],
},
getters: {
//总价
totalPrice(state) {
return state.goodses.filter((r) => r.ck).map((r) => r.price * r.count).reduce((a, b) => a + b, 0);
},
// 是否全选
isCkAll(state) {
return state.goodses.length>0 & state.goodses.every((r) => r.ck);
},
},
mutations: {
// 加载数据的同步方法
loadGoodses(state, val) {
state.goodses = val;
},
// 设置所有商品的状态
ckAll(state, val) {
state.goodses.forEach((r) => {
r.ck = val;
});
},
// 根据id删除商品
delGoodsById(state, id) {
let index = state.goodses.findIndex((r) => r.id == id);
state.goodses.splice(index, 1);
},
},
actions: {
// 加载数据的异步方法
loadGoodses(store, val) {
axios.get(val).then(({ data }) => {
store.commit("loadGoodses", data);
});
},
// 设置所有商品的状态
ckAll(store, val) {
store.commit("ckAll", val);
},
// 根据id删除商品
delGoodsById(store, id) {
store.commit("delGoodsById", id);
},
},
};
<template>
<div class="shopcart">
<h4>购物车</h4>
<table>
<thead>
<tr>
<th>
<input type="checkbox" v-model="isCkAll" />
</th>
<th>名称</th>
<th>图片</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="item in goodses" :key="item.id">
<td>
<input type="checkbox" v-model="item.ck" />
</td>
<td>{{ item.name }}</td>
<td>
<img :src="item.img" />
</td>
<td>{{ item.price }}</td>
<td>
<button @click="item.count--" :disabled="item.count === 1">
-
</button>
<input type="text" v-model="item.count" />
<button @click="item.count++" :disabled="item.count === 10">
+
</button>
</td>
<td>{{ item.price * item.count }}</td>
<td>
<button @click="delGoodsById(item.id)">删除</button>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="7">
<span>总价:</span>
<span>{{ totalPrice }}</span>
</td>
</tr>
</tfoot>
</table>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
name: "Shopcart",
computed: {
...mapState("shopcart", ["goodses"]),
...mapGetters("shopcart", ["totalPrice"]),
// 定义可以修改的计算属性
isCkAll: {
set(val) {
this.ckAll(val);
},
get() {
return this.$store.getters["shopcart/isCkAll"];
},
},
},
methods: {
// 映射actions对应的方法
...mapActions("shopcart", ["ckAll", "delGoodsById"]),
},
// 页面挂载完成
mounted() {
this.$store.dispatch("shopcart/loadGoodses", "data/shopcart.json");
},
};
</script>
四、key
v-for绑定key值,为什么不建议使用index作为key值?
如果只是展示列表数据,key值可以是索引;如果列表中的数据会经常发生变化,特别是列表数据的位置会发生变化,这时候key一定要设置为对象身上的唯一属性,比如:学号、工号、身份证号、手机号等等。目的是:当列表更新时,会大大提高列表重新渲染的性能损耗。
因为vue在渲染数据时,先将数据生成一份虚拟DOM,再将虚拟DOM生成对应的真实DOM挂载到页面中。当vue中的数据修改后,会重新生成一份虚拟DOM,并跟之前的虚拟DOM进行匹配,如果两份虚拟DOM中的key和key对应的值完全相同,不会重新生成对应的真实DOM;只有key和key对应的值不同的虚拟DOM,才会生成新的真实DOM并挂载的页面中。
<div class="home">
<button @click="add">添加</button>
<ul>
<!-- 如果key是索引,当位置发生变化时,所有数据都会重新渲染 -->
<!-- <li v-for="(item, index) in list" :key="index">{{ item }}</li> -->
<!-- 如果id是索引,当位置发生变化时,只会渲染更新的数据 -->
<li v-for="item in list" :key="item.id">{{ item }}</li>
</ul>
</div>
data() {
return {
list: [
{
id: 1001,
name: "张学友",
age: 21,
sex: "男",
},
{
id: 1002,
name: "郭富城",
age: 22,
sex: "男",
},
{
id: 1003,
name: "黎明",
age: 24,
sex: "男",
},
{
id: 1004,
name: "周润发",
age: 26,
sex: "男",
},
],
};
},
methods: {
// 添加员工的方法
add() {
let em = {
id: Date.now(),
name: "蔡依林",
age: 22,
sex: "女",
};
this.list.unshift(em);
},
}
五、$nextTick()
$nextTick():需要传一个回调函数。将回调函数里面的代码延迟到DOM渲染完毕后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
<input type="text" v-model="carName" />
<button @click="addCar">添加汽车</button>
<ul ref="list">
<li v-for="item in list" :key="item.id">
<input type="text" :value="item.name" />
</li>
</ul>
data() {
return {
// 汽车名称
carName: "",
// 汽车数组
list: [
{
id: 1001,
name: "宝马",
},
{
id: 1002,
name: "玛莎拉蒂",
},
]
};
},
methods: {
addCar() {
let car = {
id: Date.now(),
name: this.carName,
};
this.list.push(car);
this.carName = "";
// $nextTick方法,需要传一个回调函数。回调函数里面的代码在DOM更新完成后执行。
this.$nextTick(() => {
// 让最后一个li元素里面的input元素获取焦点
// focus()方法用于为元素设置焦点。
this.$refs.list.lastChild.lastChild.focus();
});
}
}
六、$forceUpdate()
$forceUpdate():进行强制更新。调用这个方法会更新视图和数据,触发updated生命周期。
<button @click="person.name = '张三'">修改姓名</button>
<button @click="addSex">添加性别</button>
<div>{{ person }}</div>
data() {
return {
person: {
name: "张学友",
age: 20,
}
};
},
methods: {
addSex() {
// 添加响应式属性
// this.$set(this.person,"sex","男")
// 直接添加的属性,不具备响应式
this.person.sex = "男";
// 通过$forceUpdate()方法,迫使vue实例重新渲染
this.$forceUpdate();
}
}
七、自定义指令
自定义指令就是一个方法,方法的第一个参数传递的是指令所在的DOM元素,方法的第二个参数是给指令绑定的数据。
1、局部指令
(1)定义指令
data() {
return {
car:'<h2>保时捷卡宴真好看</h2>'
}
},
// 定义局部指令,所有的指令背后都是在操作DOM,我们将这种功能称之为:造轮子。
directives:{
// 注册一个局部自定义指令 'v-red',设置字体颜色为红色
red:function(el){
el.style.color="red"
},
// 注册一个局部自定义指令 'v-myhtml',渲染html标签数据
myhtml(el,bind){
el.innerHTML = bind.value
}
}
(2)使用指令
<div v-red>好好学习</div>
<div v-myhtml="car"></div>
2、全局指令
(1)定义指令
// 定义全局自定义指令
import Vue from 'vue'
Vue.directive('mycolor',function(el,bind){
el.style.color = bind.value
})
(2)main.js文件中导入指令
// 导入全局自定义指令
import './directives'
(3)使用指令
<divv-color="'red'">好好学习Vue</div>
八、自定义插件
1、定义插件
定义一个插件,插件就是将给Vue添加的全局成员,归类到一起去,这样做利于后期维护。
插件本质上就是一个对象,该对象中必须包含一个install方法,方法的第一个参数是Vue,第二个参数是配置对象。install()方法,会在use的时候执行。Vue.use(插件),这里的Vue会作为install方法的第一个参数。
// 插件本质上就是一个对象
export default {
// 该对象中必须包含一个install()方法
install:function(Vue,options){
// 可以直接给Vue添加成员
Vue.sayHi = function(){
console.log('大家好!我是Vue');
},
Vue.msg = "欢迎使用插件",
// 可以在Vue的原型上扩展成员
Vue.prototype.sayHello = function(){
console.log('哈哈!我是Vue原型上的方法');
},
// 给Vue混入成员
Vue.mixin({
data() {
return {
plane:{
name:'奔驰',
price:'100W'
}
}
},
methods: {
showPlane(){
console.log(this.plane.name,this.plane.price);
}
},
}),
// 注册全局组件
Vue.component('b-box', {
// 在脚手架环境中,只能通过渲染函数定义全局组件
render(h) {
return h('div',this.$slots.default)
},
}),
// 注册全局指令
Vue.directive('bgcolor', function(el,bind){
el.style.backgroundColor = bind.value
})
}
}
2、导入插件
// 导入自定义插件
import myPlugin from './plugins'
// 注意:一定要use
Vue.use(myPlugin)
3、使用插件
<!-- 调用插件中定义的vue原型上的方法 -->
<button @click="sayhello">sayHello</button>
<!-- 调用插件中定义的vue方法 -->
<button @click="sayHi">sayHello</button>
<!-- 调用插件中定义的混入成员 -->
<button @click="showPlane">showPlane</button>
<div v-bgcolor="'lightblue'">我是淡蓝色</div>
<b-box>哈哈</b-box>