模块三:Vuex 数据流管理及Vue.js 服务端渲染(SSR)(一)Vuex状态管理

前言:本篇文章主要对vue组件之间的通信方式进行总结,并且探索vuex状态管理的核心概念和基本使用,以及自己模拟实现一个vuex。并不是任何项目都适合使用vuex,只有中大型项目才适合使用vuex。

一、vue 组件间通信方式

组件间通信方式有三种

(一)父组件给子组件传值

子组件中通过 props 接收数据
props的定义
👁️‍🗨️ 可以是一个数组,数组中的元素是要接受的数据标识符,是字符串的形式,例如:props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
👁️‍🗨️ 也可以是一个对象,key是要接受的数据标识符,value是数据类型

<template>
  <h1>{{ msg }}</h1>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

父组件中给子组件通过相应属性传值
1、通过import引入子组件
2、通过components注册子组件
3、在模版中使用子组件,通过属性传值
可以传递静态值,也可以使用v-bind传递动态数据

<template>
    <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'HomeView',
  components: {
    HelloWorld
  }
}
</script>

(二)子组件给父组件传值

子组件给父组件传值需要使用自定义事件。自定义事件,就是类似于clickkeypress等事件,使用v-on监听。只不过内置的事件通过鼠标等操作触发,自定义事件需要使用vm.$emit()方法手动触发。首先需要确定一个自定义事件的名称,这里我们叫message
父组件
在使用子组件的时候,使用v-on监听自定义事件,并且设置回调函数。注意v-on:message绑定的是回调函数的定义,而不是调用哦,不能加小括号。
v-on:可以简写为@

<template>
    <HelloWorld v-on:message="onPostMessage"/>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'HomeView',
  components: {
    HelloWorld
  },
  methods: {
  	// 自定义事件的回调处理函数
    onPostMessage(message) {
      console.log(message)
    }
  }
}
</script>

子组件
自定义触发事件的时机,例如在按钮点击的时候触发自定义事件。vm.$emit()接受两个参数,参数一是自定义事件的名称,参数二是要传递给自定义事件的参数。

<template>
  <div>
    <button v-on:click="postMessage">给父组件传值</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  methods: {
    postMessage() {
      this.$emit('message', 'Hello World')
    }
  }
}
</script>

(三)不相关组件间的传值

这种方法是通用的组件传值方法。需要创建一个额外的的Vue实例,这里管它叫bus,用来管理自定义事件,通常定义到eventBus.js文件中
eventBus.js

import Vue from "vue";
const bus = new Vue();
export default bus;

发出数据的组件需要使用bus.$emit()来触发自定义事件
发出数据的组件

<template>
  <div class="about">
    <button @click="postMessage">给其他组件传值</button>
  </div>
</template>
<script>
import bus from '@/eventBus.js'
export default {
  name: 'AboutView',
  methods: {
    postMessage() {
      // 触发自定义事件
      bus.$emit('message', 'Hello World')
    }
  }

}
</script>

接收数据的组件需要使用bus.$on()监听自定义事件

<template>
    <h1>HomeView</h1>
</template>

<script>
// @ is an alias to /src
import bus from '@/eventBus.js'

export default {
  name: 'HomeView',
  created() {
    // 监听自定义事件
    bus.$on('message', this.onPostMessage)
  },
  methods: {
    onPostMessage(message) {
      console.log(message)
    }
  }
}
</script>

二、vuex 核心概念和基本使用

• vuex 是专门为 vue.js 设计的状态管理库
• vuex 采用集中式的方式存储需要共享的状态
• vuex 的作用是进行状态管理,解决复杂组件通信,数据共享
• vuex 集成到了devtools 中,提供了 time-travel 时光旅行、历史回滚功能
• Vuex Time-travel 是一个 Vuex 插件,用于在 Vue.js 应用程序中实现状态的时间旅行功能。时间旅行是指能够回溯和查看应用程序状态的历史记录。在开发过程中,这对于调试和理解状态变化非常有用。Vuex Time-travel 插件通过记录每个状态变化的快照,允许开发者在不影响当前应用程序状态的情况下,回溯到过去的状态。

(一)Vuex核心概念

Vuex集中式管理全局状态,首先需要一个管理状态的仓库,通常称为Store仓库,每一个应用只有一个Store。Store中存储着几个变量,包括State、Getter、Mutation、Action、Module。

  • State存放需要统一管理的全局状态,并不是所有的变量都需要存放到State中,单个组件中维护的变量还是可以放在组件中单独维护。
  • Getter类似于计算属性,其中的定义是函数的形式,函数的返回值是对于某个全局状态进行处理后的结果。
getters: {
  doneTodos (state) {
    return state.todos.filter(todo => todo.done)
  }
}
  • Mutation类似于事件,这是更改 Vuex 的 store 中的状态的唯一的地方。
  • Action的定义类似于Mutation,其中可以进行各种操作,允许异步操作。
  • Module当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
mutations: {
  increment (state) {
    // 变更状态
    state.count++
  }
}

下面这张图很好的解释了Vuex的几个核心概念之间的关系。state是我们管理的全局状态,我们可以将全局状态渲染到组件Vue Components中。当在组件内部进行一些操作需要修改全局状态的时候,就需要通过dispatch()方法触发action中定义的事件,俗称”分发 Action“。action中定义的事件可以进行异步的操作,比如向服务器请求数据,以及进行其他的操作。当需要修改state的时候,通过commit()方法”提交Mutation“。在Vuex中,只有mutation中定义的方法能直接修改state中的数据,mutation中的修改是同步的。
在这里插入图片描述

(二)Vuex基本使用

1、基本结构

我们在使用vue create 项目名创建Vue项目的时候,vue-cli会提示我们是否安装vuex,只要选择了安装,vue-cli就会帮我们安装好vuex,并且初始化一个store目录,还为我们初始化好了一份基础代码
src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
// 注册插件
Vue.use(Vuex)
// 创建并导出Vuex.Store对象
export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

src/main.js中也为我们引入vuex并且给Vue实例注入store选项

import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
2、State用法

假设在State中定义了两个全局状态
src/store/index.js

state: {
  name:'zs',
  age:18
},

🟠 基础使用:vuex会将$store直接加到Vue实例上,所以在组件中可以直接通过$store使用State中的数据,这种方式不需要使用import引入store

<template>
  <div class="about">
    {{$store.state.name}}
    {{$store.state.age}}
  </div>
</template>

🟠 更加方便的方式:上边的使用方式如果组件使用到很多的数据,那么$store.state这个前缀就需要重复很多次。vuex为我们提供了一个mapState()方法,这个方法类似于计算属性,可以将state中存储的数据转换为当前组件实例上的数据,并且保证数据的响应式。
mapState()的使用有两种传参方式,
一种是传一个数组,数组中存放字符串形式的数据描述符,要使用哪个属性,就增加哪个属性;

mapState(['name','age'])

第二种是传递一个对象,这种方式支持对state中的属性进行重命名,避免和当前组件中的属性重名,key是重命名后的属性名,value是一个方法,接收state,返回对应属性。在这个方法里也可以对目标属性做一些操作并且返回操作后的结果。

mapState({
  myname:state=>state.name,
  myage:state=>state.age
})

mapState()的返回值是对象的形式
在这里插入图片描述
在组件的computed计算属性中,要对属性进行重新计算,就需要定义成属性名:处理函数的形式。所以这里需要将mapState()的返回值解构赋值放到computed中。在模版中就可以直接使用重命名后的属性名获取state中的数据

<template>
  <div class="about">
    {{myname}}
    {{myage}}
  </div>
</template>
<script>
import {mapState} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    // ...mapState(['name','age'])
    ...mapState({
      myname:state=>state.name,
      myage:state=>state.age
    })
  }
}
</script>
3、Getter用法

Getter的定义的形式是函数,接收state对象作为参数,对某个属性进行计算,返回处理后的结果。

getters: {
  getAgeAdd10(state){
      return state.age + 10
  }
},

在使用的时候,直接通过$store.getters.方法名的形式就可以获取计算后的属性,并不需要将定义的这个函数执行再赋值。

<div class="about">
  {{myname}}
  {{$store.getters.getAgeAdd10}}
</div>

Getters中的属性也可以像State一样,通过Vuex提供的mapGetters()方法映射到组件实例的计算属性中,mapGetters的使用类似于mapState()

<template>
  <div class="about">
    {{myname}}
    {{getAgeAdd10}}
  </div>
</template>
<script>
import {mapState,mapGetters} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    ...mapState({
      myname:state=>state.name
    }),
    ...mapGetters([
      'getAgeAdd10'
    ])
  }
}
</script>
4、Mutation用法

Mutation里面存储的是对State进行处理的处理函数。Mutation类似于注册事件的作用,注册的事件都是同步事件,因为需要确保获取的State是实时的State。Mutation中注册的事件是修改State的唯一的地方。使用Mutation注册的事件不能直接调用事件,而是需要使用Store的commit()方法,提交事件
store/index.js

mutations: {
    // payload 载荷,是调用该方法时传递的额外参数
    changeName(state, payload) {
        state.name = payload
    }
},

组件实例中

<template>
  <div>
    <div class="about">
      {{name}}
    </div>
    <button @click="changeName">changeName</button>
  </div>

</template>
<script>
import {mapState} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    ...mapState(['name'])
  },
  methods:{
    changeName(){
      this.$store.commit('changeName','lisi')
    }
  }
}
</script>

类似于mapStatemapGetters,vuex也提供了Mutation映射到当前组件实例上。但是需要注意的是,Mutation中的事件应该映射到methods中,调用方法只需要穿一个额外参数就可以,默认state会作为第一个参数传递给Mutation事件。

<template>
  <div>
    <div class="about">
      {{name}}
    </div>
    <button @click="changeName('lisi')">changeName</button>
  </div>

</template>
<script>
import {mapState} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    ...mapState(['name'])
  },
  methods:{
    ...mapMutations(['changeName']),
  }
}
</script>
5、Action用法

Action中的方法可以进行各种异步操作,最后修改State需要提交Mutation。
store/index.js

actions: {
    // context 上下文,相当于 store 对象
    changeNameAsync(context, payload) {
        setTimeout(() => {
        	// 提交Mutation,调用Mutation中注册的事件修改State
            context.commit('changeName', payload)
        }, 2000)
    }
},

使用Action中的方法需要使用$store.dispatch(方法名,参数)方法

<template>
  <div>
    <div class="about">
      {{name}}
    </div>
    <button @click="changeNameAsync">changeName</button>
  </div>
</template>
<script>
import {mapState} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    ...mapState(['name'])
  },
  methods:{
    changeNameAsync(){
      this.$store.dispatch('changeNameAsync','lisi')
    },
  }
}
</script>

类似的还有mapActions

<template>
  <div>
    <div class="about">
      {{name}}
    </div>
    <button @click="changeNameAsync('lisi')">changeName</button>
  </div>
</template>
<script>
import {mapState, mapActions} from 'vuex'
export default {
  name: 'AboutView',
  computed:{
    ...mapState(['name'])
  },
  methods:{
    ...mapActions(['changeNameAsync'])
  }
}
</script>
6、Module用法

如果项目非常大,全局状态特别多,使用一个状态树进行数据管理就会非常的大。此时我们可以将数据分割成多个模块,每个模块负责不同的功能。例如下列示例项目中有两个组件,一个是About.vue,一个是Home.vue,分别给他们依赖的数据创建一个模块,文件目录结构如下:
在这里插入图片描述
模块文件基本内容store/home.js

const state = {}
const getters = {}
const mutations = {}
const actions = {}
const modules = {} // 如果有嵌套子模块
export default {
    state,
    getters,
    mutations,
    actions,
    modules
}

然后需要在vuex的根文件中注册模块
store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import about from './modules/about'
import home from './modules/home'

Vue.use(Vuex)
const store = new Vuex.Store({
    modules: {
        about,
        home
    },
})
console.log(store)

export default store

此时会将abouthome这两个模块挂载到store.state上,可以通过store.state.about访问模块中的属性。模块中的方法会直接挂载到store._mutations身上。我们打印一下store,可以看到定义在模块中的Mutations和Actions会和主模块中的Mutations和Actions合并,一起挂载到_mutations_actions属性上。
在这里插入图片描述
在组件实例的模版中使用模块中的状态时,需要通过$store.state.模块名.状态名使用,提交模块中的Mutation时,只需要通过$store.commit('方法名',参数)来提交。下面例子在about模块中定义了全局状态name和Mutation方法changeName
组件实例

<template>
  <div>
    <div class="about">
      {{$store.state.about.name}}
    </div>
    <button @click="$store.commit('changeName','lisi')">changeName</button>
  </div>
</template>
<script>
export default {
  name: 'AboutView'
}
</script>

如果多个模块中都有changeName这个Mutation方法,就会一次执行所有的同名方法。例如下面代码,在AboutHome模块中都有changeName这个Mutation方法
store/modules/about.js

const state = {
    name: 'zs',
    age: 18
}
const mutations = {
    // payload 载荷,是调用该方法时传递的参数
    changeName(state, payload) {
        console.log('about changeName')
        console.log(state)
        state.name = payload
    }
}

store/modules/home.js

const state = {}
const mutations = {
    changeName(state, payload) {
        console.log('home changeName')
        console.log(state)
        state.name = payload
    }
}

点击按钮提交changeName的时候,控制台输出:
在这里插入图片描述
两个模块中的同名的Mutation方法都执行了,但是接收的state是各自模块的state
这样的话代码就不太清晰,我们在组件实例中不知道执行的是哪个模块的Mutation方法。为了明确我们要使用的是哪个模块中的方法,我们可以在定义模块的时候,给模块增加一个属性namespaced: true,
所有模块导出的位置

export default {
    namespaced: true,
    state,
    getters,
    mutations,
    actions,
    modules
}

先讲一下mapState的另一种用法,接受两个参数,第一个参数用来指定属性的模块,第二个属性用来指定要映射的属性;mapMutation也类似
组件实例中

<template>
  <div>
    <div class="about">
      {{name}}
    </div>
    <button @click="$store.commit('changeName','lisi')">changeName</button>
  </div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'AboutView',
  computed: {
    ...mapState('about', ['name'])
  },
  methods: {
    ...mapMutations('about', ['changeName'])
  }
}
</script>
7、Vuex严格模式

在使用Vuex的过程中我们约定修改全局状态只在Mutation方法中修改,但这仅仅是一个约定,程序并没有限制,仍然可以在组建实例中获取state并重新赋值。对此Vuex提出了严格模式的概念,当开启了严格模式之后,在组件实例中修改全局状态就会报错。开启严格模式只需要在创建store的时候,增加一个配置项strict: true,需要注意的是,不要在生产环境下开启严格模式,因为严格模式会深度检查状态变更导致性能缓慢。所以这个配置项可以设置为strict: process.env.NODE_ENV !== 'production',让构建工具自动检测环境来决定是否开启严格模式。当开启严格模式之后,我们在组件实例的created生命周期中去修改模块的状态:

created () {
  console.log(this.$store)
  this.$store.state.about.name = 'zhangsan'
},

控制台就会报出错误信息
在这里插入图片描述
但即使是报错了,数据也已经修改了
在这里插入图片描述

8、Vuex插件

Vuex插件插件就是一个函数,接收store作为参数,稍微看一下vuex源码中对plugins做了什么,就是plugins遍历执行,并且把当前store对象作为参数传递给每一个plugin

var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
// apply plugins  this$1就是当前store对象
plugins.forEach(function (plugin) { return plugin(this$1); });

Vuex提供了subscribe方法,通过store.subscribe()方法订阅了状态的变化。subscribe方法的回调函数接收两个参数:mutationstatemutation参数是一个描述mutation的对象,包含了type、payload等属性。state参数是当前的状态对象。在每次Mutation方法执行完毕就会触发store.subscribe()订阅的方法。
插件的注册有两步,初始化和注册

const myPlugin = store => {
    // 当 store 初始化后调用
    store.subscribe((mutation, state) => {
        // 每次 mutation 之后调用
        // mutation 的格式为 { type, payload }
        console.log('myPlugin')
        console.log(mutation)
        console.log(state)
    })
}
const store = new Vuex.Store({
    // 省略若干行代码
    plugins: [myPlugin]
})

每个Mutation方法执行完毕都会触发这个回调函数
在这里插入图片描述
适用于在数据变化后都需要执行的操作,比如缓存最新状态、提交修改

三、模拟实现 vuex

(一)结构分析

要实现一个自己的vuex,首先需要分析一下vuex的结构和功能。
👾 首先vuex是一个被导出的对象,可以通过import导入
👾 vuex具有install()方法,可以通过Vue.use()安装注册
👾 vuex具有Store构造方法,可以通过new Vuex.Store()创建store实例,store实例具有stategettersmutations等属性
👾 Store实例具有commit()方法和dispatch()方法,分别用来触发mutationsactions中的函数
基本结构就是拥有一个Store类和一个install()方法,install()方法要接收Vue构造函数作为参数,并且需要一个变量来保存Vue构造函数
src/myvuex/index.js

// 用于保存Vue
let _Vue = null;
class Store {
}

function install(Vue) {
    _Vue = Vue;
}

export default {
    Store, install
}

(二)install()

每一个Vue插件都需要有一个install()方法。首先我们要弄明白install()方法要做什么事情。在任何一个组件模版中,我们都可以通过下面的方法通过组件实例的$store属性访问vuex定义的数据

<button @click="$store.commit('changeName','lisi')">changeName</button>

所以,install()方法应该将vuex定义的数据挂载到Vue实例上,要挂载到每一个Vue实例上,就需要挂载到Vue.prototype上,直接通过原型链让所有的Vue实例继承。由于install()方法执行的时候,Vue根组件可能还没有实例化,所以要使用混入,在Vue实例的beforeCreate生命周期执行挂载操作,此时就是根组件的beforeCreate生命周期。

function install(Vue) {
    _Vue = Vue;
    // 把$store加入到所有的Vue实例上
    _Vue.mixins({
        beforeCreate() {
            if (this.$options.store) {
                _Vue.prototype.$store = this.$options.store
            }
        }
    })
}

(三)Store类

Store类首先需要一个构造函数,接收选项对象optionsoptions中包含store实例的属性。首先通过结构赋值获取这几个属性,并且设置一个初始值{},避免没有传入参数是为空。

constructor(options) {
    // 解构赋值获取用户传入的选项,如果没有传入就初始化为{}
    const {
        state = {},
        getters = {},
        mutations = {},
        actions = {}
    } = options
}

这几个属性分别进行什么处理呢?我们要从属性的使用方法入手。
state属性是一个响应式数据,可以直接在模版中绑定,所以我们使用_Vue.observable()方法,将state转换成响应式数据,并存放到Store实例的state属性上。

this.state = _Vue.observable(state)

getters是一个对象,里面存放的是很多方法,这些方法接收state作为参数,通过对state中的某个属性做简单的处理然后返回。getters的使用,是直接通过属性名的方式访问,当访问该属性的时候,才会执行处理方法。虽然用户传进来的getters的属性是函数,但是我们要通过Object.defineProperty()将方法转换为getter/setter存储到getters中,并且在该属性的get访问器中,执行用户传进来的方法,返回处理后的结果。

this.getters = Object.create(null)
// getters是用户传进来的getters,里面定义的都是方法
Object.keys(getters).forEach(key => {
	// key:用户定义的方法名
    Object.defineProperty(this.getters, key, {
        // 获取属性的时候获取的其实是get指定的函数返回的结果
        get: () => getters[key](state)
    })
})

mutationsactions
mutationsactions存储为私有属性

this._mutations = mutations;
this._actions = actions;

commit()dispatch()
commit()方法用来提交Mutation,其实就是找到Mutation中定义的方法,然后执行。接收两个参数,第一个参数是要执行的Mutation函数的方法名,第二个参数是要传递给Mutation方法的参数。另外,Mutation方法统一都是接收state作为第一个参数,commit()指定的参数作为第二个参数。

// commit方法,用来提交mutation
commit(type, payload) {
    this._mutations[type](this.state, payload)
}

dispatch()方法和commit()方法类似,用来分发Action,区别在于,Action方法接收的第一个参数不是全局状态State,而是context上下文,这里为了简便,直接传递了this,也就是当前Store实例。

// dispatch方法,用来分发action
dispatch(type, payload) {
    this._actions[type](this, payload)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值