一:组件间通信方式
state:驱动应用的数据源。
view:以声明方式将state映射到视图。
actions:响应在view上的用户输入导致的状态变化。
状态管理:
组件间的通信方式:
- 父组件给子之间的传值(父子组件)
1. 子组件中通过props接收数据。2. 父组件中给子组件通过相应属性传值
子组件
<template>
<div>
<h1>Props Down Child</h1>
<h2>{{ title }}</h2>
</div>
</template>
<script>
export default {
// props接收父组件传递的参数
// props: ['title'],
props: {
title: String
}
}
</script>
父组件
<template>
<div>
<h1>Props Down Parent</h1>
// 传递title给子组件
<child title="My journey with Vue"></child>
</div>
</template>
<script>
import child from './01-Child'
export default {
components: {
child
}
}
</script>
- 子组件给父组件传值
子组件:
this.$emit('enlargeText', 0.1)
<template>
<div>
<h1 :style="{ fontSize: fontSize + 'em' }">Props Down Child</h1>
<button @click="handler">文字增大</button>
</div>
</template>
<script>
export default {
props: {
fontSize: Number
},
methods: {
handler () {
// 在子组件中使用父组件传递过来的事件,并传递值
this.$emit('enlargeText', 0.1)
}
}
}
</script>
父组件:
<template>
<div>
<h1 :style="{ fontSize: hFontSize + 'em'}">Event Up Parent</h1>
这里的文字不需要变化
// @enlargeText="enlargeText" 传递事件给子组件
<child :fontSize="hFontSize" @enlargeText="enlargeText"></child>
<child :fontSize="hFontSize" @enlargeText="enlargeText"></child>
<child :fontSize="hFontSize" @enlargeText="hFontSize += $event"></child>
</div>
</template>
<script>
import child from './02-Child'
export default {
components: {
child
},
data () {
return {
hFontSize: 1
}
},
methods: {
enlargeText (size) {
this.hFontSize += size
}
}
}
</script>
- 不相关组件之间的传值
也是使用事件的方式:bus:事件中心
eventBus.js
import Vue from 'vue'
export default new Vue()
子组件1:
bus.$emit('numchange', this.value) bus.$emit触发事件, numchange:自定义事件名,this.value传递的参数
<template>
<div>
<h1>Event Bus Sibling01</h1>
<div class="number" @click="sub">-</div>
<input type="text" style="width: 30px; text-align: center" :value="value">
<div class="number" @click="add">+</div>
</div>
</template>
<script>
import bus from './eventbus'
export default {
props: {
num: Number
},
created () {
this.value = this.num
},
data () {
return {
value: -1
}
},
methods: {
sub () {
if (this.value > 1) {
this.value--
// bus.$emit('numchange', this.value)触发事件
// numchange:自定义事件名,this.value传递的参数
bus.$emit('numchange', this.value)
}
},
add () {
this.value++
bus.$emit('numchange', this.value)
}
}
}
</script>
<style>
.number {
display: inline-block;
cursor: pointer;
width: 20px;
text-align: center;
}
</style>
子组件2
bus.$on:注册:自定义事件numchange,() => {}:回调函数
<template>
<div>
<h1>Event Bus Sibling02</h1>
<div>{{ msg }}</div>
</div>
</template>
<script>
import bus from './eventbus'
export default {
data () {
return {
msg: ''
}
},
created () {
bus.$on('numchange', (value) => {
this.msg = `您选择了${value}件商品`
})
}
}
</script>
- 组件间通信方式回顾-通过 ref 获取子组件
ref的2个作用:
1. 在普通的html标签上使用ref,获取到的是DOM
2. 在组件标签上使用ref,获取到的是组件实例
child.vue
<template>
<div>
<h1>ref Child</h1>
<input ref="input" type="text" v-model="value">
</div>
</template>
<script>
export default {
data () {
return {
value: ''
}
},
methods: {
focus () {
// 在普通的html 上使用ref
this.$refs.input.focus()
}
}
}
</script>
parent.vue
<template>
<div>
<h1>ref Parent</h1>
<child ref="c"></child>
</div>
</template>
<script>
import child from './04-Child'
export default {
components: {
child
},
mounted () {
// 在组件上使用ref,获取到的是组件的实例
this.$refs.c.focus()
this.$refs.c.value = 'hello input'
}
}
</script>
- 状态管理方案 - Vuex
Vuex:集中式的状态管理
在组件中不能直接更改状态的属性,如果想要更该需要在store中actions中的定义函数修改,这样做的好处是:能记录store中的数据的所有的变更。
解决的问题:多个视图依赖同一状态
来自不同视图的行为需要变更同一状态
store.js
// state: 存储的状态
// setUserNameAction:通过视图和用户交互的时候更改状态用的。
// debug:方便调试,如果为true,在通过action修改数据的时候会打印日志。
export default {
debug: true,
state: {
user: {
name: 'xiaomao',
age: 18,
sex: '男'
}
},
setUserNameAction (name) {
if (this.debug) {
console.log('setUserNameAction triggered:', name)
}
this.state.user.name = name
}
}
在组件中使用:
- 引入store
- 使用store.state.user
<template>
<div>
<h1>componentA</h1>
user name: {{ sharedState.user.name }}
<button @click="change">Change Info</button>
</div>
</template>
<script>
// 1. 引入store
import store from './store'
export default {
methods: {
change () {
// 使用store中的action更改数据
store.setUserNameAction('componentA')
}
},
data () {
return {
privateState: {},
// 使用:store.state
sharedState: store.state
}
}
}
</script>
vuex核心概念和基本使用
什么是vuex?
vuex是专门为vuejs设计的状态管理库。
vuex采用集中式的方式存储需要共享的状态。
vuex的作用是进行状态管理,解决复杂组件通信,数据共享。
vuex集成到了devtools中,提供了time-travel时光旅行历史回滚功能。
什么情况下使用vuex?
非必要的情况不需要使用vuex。
大型的单页应用程序:1.多个视图依赖同一状态。2.来自不同视图的行为需要变更同一状态。
vuex的核心概念:
上述图的描述:
state:state中存储数据
vue components:把状态绑定到组件中渲染到用户界面展示给用户。用户可以和视图交互,例如点击按钮(加入购物车):通过dispatch分发actions,此处不直接提交mutations,因为actions中可以做异步操作,(购买的时候需要发送异步请求,异步请求结束之后,再通过提交mutation记录状态的更改,)
mutations: mutations必须是同步的,所有状态的更改都要通过mutations,这样做的目的是:通过mutations可以追踪到所有状态的改变,阅读代码的时候更容易分析应用内部的状态改变,还可以记录每一次状态的改变,实现高级的调试功能。
核心概念:
Store:
state:响应式的状态
Getter: 有点类似于vue中的计算属性,方便从一个属性派生出其他的值。内部可以对计算的结果进行缓存,只有当依赖的状态发生改变的时候才会重新计算。
Mutation:状态的变化必须通过提交mutation来完成。
Actions: action和mutation类似,不同的是action可以进行异步的操作。内部改变状态的时候都需要提交mutation
Module:模块,当数据变得非常大的时候可以采用模块的方式。
如果使用?整体的代码结构
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
})
main.js
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
console.log(store)
使用state:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count:0,
msg: 'hello vuex'
},
getters: {
// 把msg倒序输出
reverseMsg(state) {
return state.msg.split('').reverse().join()
}
},
mutations: {
},
actions: {
},
})
在组件中使用:
方法1: $store.state.count
<template>
<div id="app">
count:{{ $store.state.count }}
msg: {{ $store.state.msg }}
{{count}}
{{msg}}
</div>
<template>
方法2:mapState
import { mapState } from 'vuex'
computed: {
// 方法1:写成数组的方式
//...mapState(['count', 'msg'])
// 方法2:函数方式的写法
count: state => state.count,
msg: state => state.msg,
// 方法3:写成对象的形式,可以解决命名冲突的问题
...mapState({num: 'count', message: 'msg'}),
}
Gutter(在视图中显示数据): 类似于计算属性,想把store中的数据处理一下再显示到页面上。
getters: {
// 把msg倒序输出
reverseMsg(state) {
return state.msg.split('').reverse().join()
}
},
在组件中使用:
方式1: 直接在模版中使用:$store.getters.reverseMsg方式2:
使用mapGetters`
<div>
{{$store.getters.reverseMsg}}
{{reverseMsg}}
</div>
<script>
import { mapGetters} from 'vuex'
computed: {
...mapGetters([reverseMsg])
}
</script>
Mutation(修改状态):如何修改状态?状态的修改必须经过提交mutation,mutation必须是同步执行,这样可以保证能够在mutation中收集到所有的状态修改。不要在mutation中使用异步方式,如果需要使用异步则使用action
用法:
// 点击【增加】按钮
mutations: {
increate(state, paylod) {
state.count + = paylod
}
}
在组件中使用:
方式1:$store.commit('increate', 2)
<button @click="$store.commit('increate', 2)">增加</button>
方式2: mapMutations
<button @click="increate(3)">增加</button>
import {mapMutations} from 'Vuex'
method:{
...mapMutations(['increate']),
}
Action: 如果需要使用异步操作,则使用action,当异步结束后如果需要更改状态,需要提mutation来修改state。因为所有的状态更改都是使用mutation。例如:如果需要获取商品数据,则需要在action 中发送请求,异步执行完毕,获取到商品数据之后,需要再提交mutation,把数据记录到state中。
Action使用:
actions: {
increateAsync(context, payload) {
// setTimeout是异步任务
setTimeout(() => {
context.commit('increate', payload)
}, 2000)
}
}
在组件中使用:
方法1:$store.dispatch('increateAsync', 5)
方法2: mapActions
<button @click="$store.dispatch('increateAsync', 5)">Action</button>
<button @click="increateAsync(6)">Action</button>
import { mapActions } from 'vuex'
methods: {
...mapActions(['increateAsync']),
}
Module:模块化处理state
目录结构:
index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 模块化处理state
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
msg: 'Hello Vuex'
},
getters: {
reverseMsg (state) {
return state.msg.split('').reverse().join('')
}
},
mutations: {
increate (state, payload) {
state.count += payload
}
},
actions: {
increateAsync (context, payload) {
setTimeout(() => {
context.commit('increate', payload)
}, 2000)
}
},
modules: {
products,
cart
}
})
modules/cart.js
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
modules/products.js
const state = {
products: [
{ id: 1, title: 'iPhone 11', price: 8000 },
{ id: 2, title: 'iPhone 12', price: 10000 }
]
}
const getters = {}
const mutations = {
setProducts (state, payload) {
state.products = payload
}
}
const actions = {}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
在组件中使用模块的方法和state属性
方法1:$store.state.products.products. 第一个products指的是模块的名字,第2个products指的是state中的属性
方法2:mapMutations
products: {{ $store.state.products.products }} <br>
<button @click="$store.commit('setProducts', [])">Mutation</button>
方法2:
products: {{ products }} <br>
<button @click="setProducts([])">Mutation</button>
import {mapMutations} from 'vuex'
computed: {
// 需要注意:mapState的第一个参数:products指的是模块的名字
// 第2个参数:['products']指的是state中的属性
...mapState('products', ['products'])
},
methods: {
// 需要注意:mapMutations的第一个参数:products指的是模块的名字
// 第2个参数:['setProducts']指的是mutation中的方法名字
...mapMutations('products', ['setProducts'])
}
vuex严格模式:
所有的状态更改必须经过mutation,但是这只是一个口头约定,如果在组件中$store.state.count = 1
也是可以修改的的,不会抛出错误,但是这样状态state的修改就变得不可追踪,devtools页没有办法追踪,开启严格模式之后,如果这样修改的话,就会抛出错误。
开启严格模式:strict: process.env.NODE_ENV !== 'production',
import Vue from 'vue'
import Vuex from 'vuex'
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)
export default new Vuex.Store({
// 开启严格模式
strict: process.env.NODE_ENV !== 'production',
state: {
count: 0,
msg: 'Hello Vuex'
},
getters: {
reverseMsg (state) {
return state.msg.split('').reverse().join('')
}
},
mutations: {
increate (state, payload) {
state.count += payload
}
},
actions: {
increateAsync (context, payload) {
setTimeout(() => {
context.commit('increate', payload)
}, 2000)
}
},
modules: {
products,
cart
}
})
开启之后的报错:
不要在生产模式开启,因为会深度监听影响性能。
购物车案例
购物车功能:
1.用到了node写的接口:node server.js
2.
页面:
当刷新购物车页面之后当,前购物车中的数据是存储在本地的。
项目代码地址:
三个组件:商品列表组件(products.vue),
购物车组件(cart.vue),
鼠标悬浮【我的购物车】展示购物车中的商品组件(pop-cart.vue)
商品列表组件:
store
store/modules/products.js
import axios from 'axios'
const state = {
products: [], // 商品列表
}
const getters = {}
const mutations = {
setProducts (state, payload) {
state.products = payload
}
}
const actions = {
async getProducts ({ commit }) {
const { data } = await axios({
method: 'GET',
url: 'http://127.0.0.1:3000/products'
})
commit('setProducts', data)
}
}
export default {
// 开启命名空间
namespaced: true,
state,
getters,
mutations,
actions
}
2.6版本以前获取插槽中的数据
<template slot-scope="scope"></template>
2.6之后
<template v-slot="scope"></template>
store/modules/cart.js
const state = {
// 购物车中的数据存储到本地存储中
cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || []
}
const getters = {
// 统计商品数量
totalCount (state) {
return state.cartProducts.reduce((sum, prod) => sum + prod.count, 0)
},
// 统计商品总价
totalPrice (state) {
return state.cartProducts.reduce((sum, prod) => sum + prod.totalPrice, 0)
},
// 统计选中商品的数量
checkedCount (state) {
return state.cartProducts.reduce((sum, prod) => {
if (prod.isChecked) {
sum += prod.count
}
return sum
}, 0)
},
// 统计选中商品的价格
checkedPrice (state) {
return state.cartProducts.reduce((sum, prod) => {
if (prod.isChecked) {
sum += prod.totalPrice
}
return sum
}, 0)
}
}
const mutations = {
// 加入购物车
addToCart (state, product) {
// 1. cartProducts 中没有该商品,把该商品添加到数组,并增加 count,isChecked,totalPrice
// 2. cartProducts 有该商品,让商品的数量加1,选中,计算小计
const prod = state.cartProducts.find(item => item.id === product.id)
if (prod) {
prod.count++
prod.isChecked = true
prod.totalPrice = prod.count * prod.price
} else {
state.cartProducts.push({
...product,
count: 1,
isChecked: true,
totalPrice: product.price
})
}
},
// 删除购物车中的商品
deleteFromCart (state, prodId) {
// 根据商品ID,找到商品在列表中的下标
const index = state.cartProducts.findIndex(item => item.id === prodId)
index !== -1 && state.cartProducts.splice(index, 1)
},
// 改变所有商品的isChecked属性
updateAllProductChecked (state, checked) {
state.cartProducts.forEach(prod => {
prod.isChecked = checked
})
},
// 改变某个商品的checked属性
updateProductChecked (state, { checked, prodId}) {
// 找到商品对象
const prod = state.cartProducts.find(prod => prod.id === prodId)
prod && (prod.isChecked = checked)
},
// 更新商品的数量和总价
updateProduct (state, { prodId, count }) {
const prod = state.cartProducts.find(prod => prod.id === prodId)
if (prod) {
prod.count = count
prod.totalPrice = count * prod.price
}
}
}
const actions = {}
export default {
// 开启命名空间
namespaced: true,
state,
getters,
mutations,
actions
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)
// vuex中的插件
const myPlugin = store => {
store.subscribe((mutation, state) => {
if (mutation.type.startsWith('cart/')) {
// 购物车中的数据存储到本地
window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))
}
})
}
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
products,
cart
},
plugins: [myPlugin]
})
views/products
<template>
<div>
<el-breadcrumb separator="/">
<el-breadcrumb-item><a href="#/">首页</a></el-breadcrumb-item>
<el-breadcrumb-item><a href="#/">商品列表</a></el-breadcrumb-item>
</el-breadcrumb>
<el-table
:data="products"
style="width: 100%">
<el-table-column
prop="title"
label="商品">
</el-table-column>
<el-table-column
prop="price"
label="价格">
</el-table-column>
<el-table-column
prop="address"
label="操作">
<!-- <template slot-scope="scope"> -->
<template v-slot="scope">
<el-button @click="addToCart(scope.row)">加入购物车</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { mapState, mapActions, mapMutations } from 'vuex'
export default {
name: 'ProductList',
computed: {
...mapState('products', ['products'])
},
methods: {
...mapActions('products', ['getProducts']),
...mapMutations('cart', ['addToCart'])
},
created () {
this.getProducts()
}
}
</script>
<style></style>
views/carts
<template>
<div>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>购物车</el-breadcrumb-item>
</el-breadcrumb>
<el-table
:data="cartProducts"
style="width: 100%"
>
<el-table-column
width="55">
<template v-slot:header>
<el-checkbox v-model="checkedAll" size="mini">
</el-checkbox>
</template>
<!--
@change="updateProductChecked" 默认参数:更新后的值
@change="updateProductChecked(productId, $event)" 123, 原来那个默认参数
当你传递了自定义参数的时候,还想得到原来那个默认参数,就手动传递一个 $event
-->
<template v-slot="scope">
<el-checkbox
size="mini"
:value="scope.row.isChecked"
@change="updateProductChecked({
prodId: scope.row.id,
checked: $event
})"
>
</el-checkbox>
</template>
</el-table-column>
<el-table-column
prop="title"
label="商品">
</el-table-column>
<el-table-column
prop="price"
label="单价">
</el-table-column>
<el-table-column
prop="count"
label="数量">
<template v-slot="scope">
<el-input-number :value="scope.row.count" @change="updateProduct({
prodId: scope.row.id,
count: $event
})" size="mini"></el-input-number>
</template>
</el-table-column>
<el-table-column
prop="totalPrice"
label="小计">
</el-table-column>
<el-table-column
label="操作">
<template>
<el-button size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
<div>
<p>已选 <span>{{ checkedCount }}</span> 件商品,总价:<span>{{ checkedPrice }}</span></p>
<el-button type="danger">结算</el-button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapGetters } from 'vuex'
export default {
name: 'Cart',
computed: {
// 购物车列表数据
...mapState('cart', ['cartProducts']),
// checkedCount:选中商品的个数
// checkedPrice:选中商品的价格
...mapGetters('cart', ['checkedCount', 'checkedPrice']),
// 全选
checkedAll: {
get () {
return this.cartProducts.every(prod => prod.isChecked)
},
// 当全选的按钮选中时,每条商品的状态需要选中
set (value) {
this.updateAllProductChecked(value)
}
}
},
methods: {
...mapMutations('cart', [
'updateAllProductChecked',
'updateProductChecked',
'updateProduct'
])
}
}
</script>
<style></style>
views/components/pop-cart.js
<template>
<el-popover
width="350"
trigger="hover"
>
<el-table :data="cartProducts" size="mini">
<el-table-column property="title" width="130" label="商品"></el-table-column>
<el-table-column property="price" label="价格"></el-table-column>
<el-table-column property="count" width="50" label="数量"></el-table-column>
<el-table-column label="操作">
<template v-slot="scope">
<el-button @click="deleteFromCart(scope.row.id)" size="mini">删除</el-button>
</template>
</el-table-column>
</el-table>
<div>
<p>共 {{ totalCount }} 件商品 共计¥{{ totalPrice }}</p>
<el-button size="mini" type="danger" @click="$router.push({ name: 'cart' })">去购物车</el-button>
</div>
<el-badge :value="totalCount" class="item" slot="reference">
<el-button type="primary">我的购物车</el-button>
</el-badge>
</el-popover>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
name: 'PopCart',
computed: {
...mapState('cart', ['cartProducts']),
...mapGetters('cart', ['totalCount', 'totalPrice'])
},
methods: {
...mapMutations('cart', ['deleteFromCart'])
}
}
</script>
<style>
</style>
vuex中的插件使用:
// 定义插件
const myPlugin = store => {
// 当store初始化后调用
// store中subscribe方法是用来订阅mutation
store.subscribe((mutation, state) => {
// 每次mutation之后调用
// mutation的格式为 {type, payload}
})
}
// 注册插件:
const store = new Vuex.Store({
plugins: [myPlugin]
})
模拟实现vuex
myVuex/index.js
let _Vue = null
class Store {
constructor (options) {
const {
state = {},
getters = {},
mutations = {},
actions = {}
} = options
// _Vue.observable(state):把数据变成响应式的数据
this.state = _Vue.observable(state)
this.getters = Object.create(null)
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](state)
})
})
this._mutations = mutations
this._actions = actions
}
commit (type, payload) {
this._mutations[type](this.state, payload)
}
dispatch (type, payload) {
this._actions[type](this, payload)
}
}
function install (Vue) {
_Vue = Vue
_Vue.mixin({
beforeCreate () {
if (this.$options.store) {
_Vue.prototype.$store = this.$options.store
}
}
})
}
export default {
Store,
install
}