vuex实现及简略解析(小结)

大家都知道vuexvue的一个状态管理器,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。先看看vuex下面的工作流程图

通过官方文档提供的流程图我们知道,vuex的工作流程,

1、数据从state中渲染到页面;

2、在页面通过dispatch来触发action

3、action通过调用commit,来触发mutation

4、mutation来更改数据,数据变更之后会触发dep对象的notify,通知所有Watcher对象去修改对应视图(vue的双向数据绑定原理)。

使用vuex

理解vuex的工作流程我们就看看vuexvue中是怎么使用的。

首先用vue-cli创建一个项目工程,如下图,选择vuex,然后就是一路的回车键

安装好之后,就有一个带有vuexvue项目了。

进入目录然后看到,src/store.js,在里面加了一个状态{count: 100},如下

import Vue from 'vue'
import Vuex from 'vuex' // 引入vuex

Vue.use(Vuex) // 使用插件

export default new Vuex.Store({
 state: {
  count: 100 // 加一个状态
 },
 getter: {

 },
 mutations: {

 },
 actions: {

 }
})

最后在App.vue文件里面使用上这个状态,如下

<template>
 <div id="app">
  这里是stort------->{{this.$store.state.count}}
 </div>
</template>

<script>
export default {
 name: 'app'
}
</script>

<style>
</style>

项目跑起来就会看到页面上看到,页面上会有100了,如下图

到这里我们使用vuex创建了一个store,并且在我们的App组件视图中使用,但是我们会有一些列的疑问。

  • store是如何被使用到各个组件上的??
  • 为什么state的数据是双向绑定的??
  • 在组件中为什么用this.$store.dispch可以触发storeactions??
  • 在组件中为什么用this.$store.commit可以触发storemutations??
  • ....等等等等

带着一堆问题,我们来自己实现一个vuex,来理解vuex的工作原理。

安装并使用store

src下新建一个vuex.js文件,然后代码如下

'use strict'

let Vue = null

class Store {
 constructor (options) {
  let { state, getters, actions, mutations } = options
 }
}
// Vue.use(Vuex)
const install = _Vue => {
 // 避免vuex重复安装
 if (Vue === _Vue) return
 Vue = _Vue
 Vue.mixin({
  // 通过mixins让每个组件实例化的时候都会执行下面的beforeCreate
  beforeCreate () {
   // 只有跟节点才有store配置,所以这里只走一次
   if (this.$options && this.$options.store) {
    this.$store = this.$options.store
   } else if (this.$parent && this.$parent.$store) { // 子组件深度优先 父 --> 子---> 孙子
    this.$store = this.$parent.$store
   }
  }
 })
}

export default { install, Store }

然后修改store.js中的引入vuex模块改成自己的vuex.js

import Vuex from './vuex' // 自己创建的vuex文件

在我们的代码中export default { install, Store }导出了一个对象,分别是installStore

install的作用是,当Vue.use(Vuex)就会自动调用install方法,在install方法里面,我们用mixin混入了一个beforeCreate的生命周期的钩子函数,使得当每个组件实例化的时候都会调用这个函数。

beforeCreate中,第一次根组件通过store属性挂载$store,后面子组件调用beforeCreate挂载的$store都会向上找到父级的$store,这样子通过层层向上寻找,让每个组件都挂上了一个$store属性,而这个属性的值就是我们的new Store({...})的实例。如下图

通过层层向上寻找,让每个组件都挂上了一个$store属性

设置state响应数据

通过上面,我们已经从每个组件都通过this.$store来访问到我们的store的实例,下面我们就编写state数据,让其变成双向绑定的数据。下面我们改写store

class Store {
 constructor (options) {
  let { state, getters, actions, mutations } = options // 拿到传进来的参数
  this.getters = {}
  this.mutations = {}
  this.actions = {}
  // vuex的核心就是借用vue的实例,因为vuex的数据更改回更新视图
  this._vm = new Vue({
   data: {
    state
   }
  })
 }
 // 访问state对象时候,就直接返回响应式的数据
 get state() { // Object.defineProperty get 同理
  return this._vm.state
 }
}

传进来的state对象,通过new Vue({data: {state}})的方式,让数据变成响应式的。当访问state对象时候,就直接返回响应式的数据,这样子在App.vue中就可以通过this.$store.state.count拿到state的数据啦,并且是响应式的呢。

编写mutations、actions、getters

上面我们已经设置好state为响应式的数据,这里我们在store.js里面写上mutations、actions、getters,如下

import Vue from 'vue'
import Vuex from './vuex' // 引入我们的自己编写的文件

Vue.use(Vuex) // 安装store
// 实例化store,参数数对象
export default new Vuex.Store({
 state: {
  count : 1000
 },
 getters : {
  newCount (state) {
   return state.count + 100
  }
 },
 mutations: {
  change (state) {
   console.log(state.count)
   state.count += 10
  }
 },
 actions: {
  change ({commit}) {
   // 模拟异步
   setTimeout(() => {
    commit('change')
   }, 1000)
  }
 }
})

配置选项都写好之后,就看到getters对象里面有个newCount函数,mutationsactions对象里面都有个change函数,配置好store之后我们在App.vue就可以写上,dispatchcommit,分别可以触发actionsmutations,代码如下

<template>
 <div id="app">
  这里是store的state------->{{this.$store.state.count}} <br/>
  这里是store的getter------->{{this.$store.getters.newCount}} <br/>
  <button @click="change">点击触发dispach--> actions</button>
  <button @click="change1">点击触发commit---> mutations</button>
 </div>
</template>

<script>
export default {
 name: 'app',
 methods: {
  change () {
   this.$store.dispatch('change') // 触发actions对应的change
  },
  change1 () {
   this.$store.commit('change') // 触发mutations对应的change
  }
 },
 mounted () {
  console.log(this.$store)
 }
}
</script>

数据都配置好之后,我们开始编写store类,在此之前我们先编写一个循环对象工具函数。

const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))
// 作用:
// 例如{a: '123'}, 把对象的key和value作为参数
// 然后就是函数运行callback(a, '123')

工具函数都准备好了,之后,下面直接县编写gettersmutationsactions的实现

class Store {
 constructor (options) {
  let { state = {}, getters = {}, actions = {}, mutations = {} } = options
  this.getters = {}
  this.mutations = {}
  this.actions = {}
  // vuex的核心就是借用vue的实例,因为vuex的数据更改回更新视图
  this._vm = new Vue({
   data: {
    state
   }
  })
  // 循环getters的对象
  myforEach(getters, (getterName, getterFn) => {
   // 对this.getters对象进行包装,和vue的computed是差不多的
   // 例如 this.getters['newCount'] = fn(state)
   // 执行 this.getters['newCount']()就会返回计算的数据啦
   Object.defineProperty(this.getters, getterName, {
    get: () => getterFn(state)
   })
  })
  // 这里是mutations各个key和值都写到,this.mutations对象上面
  // 执行的时候就是例如:this.mutations['change']()
  myforEach(mutations, (mutationName, mutationsFn) => {
   // this.mutations.change = () => { change(state) }
   this.mutations[mutationName] = () => {
    mutationsFn.call(this, state)
   }
  })
  // 原理同上
  myforEach(actions, (actionName, actionFn) => {
   // this.mutations.change = () => { change(state) }
   this.actions[actionName] = () => {
    actionFn.call(this, this)
   }
  })
  const {commit , dispatch} = this // 先存一份,避免this.commit会覆盖原型上的this.commit
  // 解构 把this绑定好
  // 通过结构的方式也要先调用这类,然后在下面在调用原型的对应函数
  this.commit = type => {
   commit.call(this, type)
  }
  this.dispatch = type => {
   dispatch.call(this, type)
  }
 }
 get state() { // Object.defineProperty 同理
  return this._vm.state
 }
 // commi调用
 commit (type) {
  this.mutations[type]()
 }
 // dispatch调用
 dispatch (type) {
  this.actions[type]()
 }
}

通过上面的,我们可以看出,其实mutationsactions都是把传入的参数,赋值到store实例上的this.mutationsthis.actions对象里面。

当组件中this.$store.commit('change')的时候 其实是调用this.mutations.change(state),就达到了改变数据的效果,actions同理。

getters是通过对Object.defineProperty(this.getters, getterName, {})
对this.getters进行包装当组件中this.$store.getters.newCount其实是调用getters对象里面的newCount(state),然后返回计算结果。就可以显示到界面上了。

大家看看完成后的效果图。

到这里大家应该懂了vuex的内部代码的工作流程了,vuex的一半核心应该在这里了。为什么说一半,因为还有一个核心概念module,也就是vuex的数据的模块化。

vuex数据模块化

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

例如下面的store.js

// 实例化store,参数数对象
export default new Vuex.Store({
 modules: {
  // 模块a
  a: {
   state: {
    count: 4000
   },
   actions: {
    change ({state}) {
     state.count += 21
    }
   },
   modules: {
    // 模块b
    b: {
     state: {
      count: 5000
     }
    }
   }
  }
 },
 state: {
  count : 1000
 },
 getters : {
  newCount (state) {
   return state.count + 100
  }
 },
 mutations: {
  change (state) {
   console.log(state.count)
   state.count += 10
  }
 },
 actions: {
  change ({commit}) {
   // 模拟异步
   setTimeout(() => {
    commit('change')
   }, 1000)
  }
 }
})

然后就可以在界面上就可以写上this.$store.state.a.count(显示a模块count)this.$store.state.a.b.count(显示a模块下,b模块的count),这里还有一个要注意的,其实在组件中调用this.$store.dispatch('change')会同时触发,根的actionsa模块actions里面的change函数。

下面我们就直接去实现models的代码,也就是整个vuex的实现代码,

'use strict'

let Vue = null
const myforEach = (obj, callback) => Object.keys(obj).forEach(key => callback(key, obj[key]))

class Store {
 constructor (options) {
  let state = options.state
  this.getters = {}
  this.mutations = {}
  this.actions = {}
  // vuex的核心就是借用vue的实例,因为vuex的数据更改回更新视图
  this._vm = new Vue({
   data: {
    state
   }
  })

  // 把模块之间的关系进行整理, 自己根据用户参数维护了一个对象
  // root._children => a._children => b
  this.modules = new ModulesCollections(options)
  // 无论子模块还是 孙子模块 ,所有的mutations 都是根上的
  // 安装模块
  installModules(this, state, [], this.modules.root)

  // 解构 把this绑定好
  const {commit , dispatch} = this
  // 通过结构的方式也要先调用这类,然后在下面在调用原型的对应函数
  this.commit = type => {
   commit.call(this, type)
  }
  this.dispatch = type => {
   dispatch.call(this, type)
  }
 }
 get state() { // Object.defineProperty 同理
  return this._vm.state
 }
 commit (type) {
  // 因为是数组,所以要遍历执行
  this.mutations[type].forEach(fn => fn())
 }
 dispatch (type) {
  // 因为是数组,所以要遍历执行
  this.actions[type].forEach(fn => fn())
 }
}

class ModulesCollections {
 constructor (options) { // vuex []
  // 注册模块
  this.register([], options)
 }
 register (path, rawModule) {
  // path 是空数组, rawModule 就是个对象
  let newModule = {
   _raw: rawModule, // 对象
   _children: {}, // 把子模块挂载到这里
   state: rawModule.state
  }
  if (path.length === 0) { // 第一次
   this.root = newModule
  } else {
   // [a, b] ==> [a]
   let parent = path.slice(0, -1).reduce((root, current) => {
    return root._children[current]
   }, this.root)
   parent._children[path[path.length - 1]] = newModule
  }
  if (rawModule.modules) {
   // 遍历注册子模块
   myforEach(rawModule.modules, (childName, module) => {
    this.register(path.concat(childName), module)
   })
  }
 }
}

// rootModule {_raw, _children, state }
function installModules (store, rootState, path, rootModule) {
 // rootState.a = {count:200}
 // rootState.a.b = {count: 3000}
 if (path.length > 0) {
  // 根据path找到对应的父级模块
  // 例如 [a] --> path.slice(0, -1) --> [] 此时a模块的父级模块是跟模块
  // 例如 [a,b] --> path.slice(0, -1) --> [a] 此时b模块的父级模块是a模块
  let parent = path.slice(0, -1).reduce((root, current) => {
   return root[current]
  }, rootState)
  // 通过Vue.set设置数据双向绑定
  Vue.set(parent, path[path.length - 1], rootModule.state)
 }
 // 设置getter
 if (rootModule._raw.getters) {
  myforEach(rootModule._raw.getters, (getterName, getterFn) => {
   Object.defineProperty(store.getters, getterName, {
    get: () => {
     return getterFn(rootModule.state)
    }
   })
  })
 }
 // 在跟模块设置actions
 if (rootModule._raw.actions) {
  myforEach(rootModule._raw.actions, (actionName, actionsFn) => {
   // 因为同是在根模块设置,子模块也有能相同的key
   // 所有把所有的都放到一个数组里面
   // 就变成了例如 [change, change] , 第一个是跟模块的actions的change,第二个是a模块的actions的change
   let entry = store.actions[actionName] || (store.actions[actionName] = [])
   entry.push(() => {
    const commit = store.commit
    const state = rootModule.state
    actionsFn.call(store, {state, commit})
   })
  })
 }
 // 在跟模块设置mutations, 同理上actions
 if (rootModule._raw.mutations) {
  myforEach(rootModule._raw.mutations, (mutationName, mutationFn) => {
   let entry = store.mutations[mutationName] || (store.mutations[mutationName] = [])
   entry.push(() => {
    mutationFn.call(store, rootModule.state)
   })
  })
 }
 // 递归遍历子节点的设置
 myforEach(rootModule._children, (childName, module) => {
  installModules(store, rootState, path.concat(childName), module)
 })
}

const install = _Vue => {
 // 避免vuex重复安装
 if (Vue === _Vue) return
 Vue = _Vue
 Vue.mixin({
  // 通过mixins让每个组件实例化的时候都会执行下面的beforeCreate
  beforeCreate () {
   // 只有跟节点才有store配置
   if (this.$options && this.$options.store) {
    this.$store = this.$options.store
   } else if (this.$parent && this.$parent.$store) { // 子组件深度优先 父 --> 子---> 孙子
    this.$store = this.$parent.$store
   }
  }
 })
}

export default { install, Store }

看到代码以及注释,主要流程就是根据递归的方式,处理数据,然后根据传进来的配置,进行操作数据。

至此,我们把vuex的代码实现了一遍,在我们App.vue的代码里添加

<template>
 <div id="app">
  这里是store的state------->{{this.$store.state.count}} <br/>
  这里是store的getter------->{{this.$store.getters.newCount}} <br/>
  这里是store的state.a------->{{this.$store.state.a.count}} <br/>
  <button @click="change">点击触发dispach--> actions</button>
  <button @click="change1">点击触发commit---> mutations</button>
 </div>
</template>

最后查看结果。

源码地址:https://github.com/naihe138/write-vuex

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 基于vue2.0+vuex的日期选择组件功能实现

    calendar vue日期选择组件 一个选择日期的vue组件 基于vue2.0 + vuex 原本是想找这样的一个组件的,查看了vuex后,发现vuex的写法还不是基于2.0的,所以就自己动手做了 demo展示&&项目中的使用 目录结构 demo 用vue-cli 的webpack-simple构建的 calendar |--dist build生成的目录 |--doc 展示图片 |--src |--assets 资源 |--components |--calendar 日期组件 |--

  • 使用Vuex实现一个笔记应用的方法

    最近开始着手学习 Vue,先大略的过了一遍官方文档,跟着敲过一部分官方文档中的 DEMO,然而还是不甚了了.在网上找到了一个入门的笔记应用,即便是入门级的应用,在学习途中依旧困难重重.特将学习作此笔记,方便今后回看,也希望能够帮到刚开始学习 Vue 的女同学 附原作者 Github 链接:https://github.com/lichenbuliren/vuex-notes-app2 预期目标 笔记具备如下基本功能 1.新增 2.删除 3.收藏 4.在全部笔记和收藏笔记间切换 5.在当前列表中进

  • Vuex实现计数器以及列表展示效果

    本篇教程将以计数器及列表展示两个例子来讲解Vuex的简单用法. 本案例github 从安装到启动初始页面的过程都直接跳过.注意安装时选择需要路由. 首先,src目录下新建store目录及相应文件,结构如下: index.js文件内容: import Vue from "vue" import Vuex from 'vuex' Vue.use(Vuex); //务必在new Vuex.Store之前use一下 export default new Vuex.Store({ state:{

  • Vuex模块化实现待办事项的状态管理

    前言 在vue里,组件之间的作用域是独立的,父组件跟子组件之间的通讯可以通过prop属性来传参,但是在兄弟组件之间通讯就比较麻烦了.比如A组件要告诉一件事给B组件,那么A就要先告诉他们的爸组件,然后爸组件再告诉B.当组件比较多,要互相通讯的事情很多的话,爸组件要管他们那么多事,很累的.vuex正是为了解决这个问题,让多个子组件之间可以方便的通讯. 项目介绍 待办事项中的一个事件,它可能拥有几个状态,未完成.已完成.已取消或被删除等.这个事件需要在这多种状态之间切换,那么使用vuex来管理也是非常

  • VUE利用vuex模拟实现新闻点赞功能实例

    回顾新闻详细页 很早我们的新闻详情页是在news-detail.vue 组件里,获取服务器数据,然后把数据保持到组件的data 里,既然我们已经用到了vuex,学习了它的state,我们就应该想到把返回的数据交给state 来存储. 1.首先在Vuex.Store 实例化的时候: state:{ user_name:"", newslist:[], newsdetail:{} }, 增加一个newsdetail 对象,newslist 数组是我们前面用来保存新闻列表数据的. 2.下面就

  • 如何使用vuex实现兄弟组件通信

    前言 vuex主要是是做数据交互,父子组件传值可以很容易办到,但是兄弟组件间传值,需要先将值传给父组件,再传给子组件,异常麻烦. 下面这篇文章就来给大家介绍下vuex兄弟组件通信的方法,下面话不多说了,来一起看看详细的介绍吧 1. 核心想法 使用vuex进行兄弟组件通信的核心思路就是将vuex作为一个store(vuex被设计的原因之一),将每个子组件的数据都存放进去,每个子组件都从vuex里获取数据,其实就是一句话--把vuex作为一个桥 2. 具体代码 父组件App.vue <templat

  • vuex实现简易计数器

    本文实例为大家分享了vue.js计数器的制作方法,供大家参考,具体内容如下 src/components Hello.vue <template> <div class="hello"> <h1>Now count is {{counterValue}}</h1> <br> </div> </template> <script> import { getCount } from '../vu

  • vuex实现登录状态的存储,未登录状态不允许浏览的方法

    基础思路就是使用vuex状态管理来存储登录状态(其实就是存一个值,例如token),然后在路由跳转前进行登录状态的判断,可以使用vue-router的全局前置守卫beforeEach,也可以使用路由独享的守卫beforeEnter. 导航守卫 正如其名,vue-router``` 提供的导航守卫主要用来通过跳转或取消的方式守卫导航.有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的. 记住参数或查询的改变并不会触发进入/离开的导航守卫.你可以通过观察 $route 对象来应对

  • 详解如何实现一个简单的 vuex

    首先我们需要知道为何要使用 vuex.父子组件通信用 prop 和自定义事件可以搞定,简单的非父子组件通信用 bus(一个空的 Vue 实例).那么使用 vuex 就是为了解决复杂的非父子组件通信. 仅仅会使用 vuex 没什么,看过文档敲敲代码大家都会.难道你就不想知道 vuex 是如何实现的?! 抛开 vuex 的源码,我们先来想想如何实现一个简单的 "vuex".有多简单呢,我不要 getter.mutation.action 等,我只要 state 就行了. 非父子组件通信 在

  • 详解使用Vue Router导航钩子与Vuex来实现后退状态保存

    不好意思,标题比较啰嗦,因为这次的流水账确实属于一个比较细节的小东西,下面详细讲: 1需求 最近在使用electron-vue开发一个跨平台的桌面端软件,刚上手写了几个页面,遇到一个问题:桌面端软件通常会有导航需求,类似下图 导航按钮 点击返回按钮,返回上一页,并且显示上页内容.其实不止App,即使普通的网页中也会有此类需求,尤其是使用vue写SPA时. 项目中的导航几乎都是采用router.push({name: 'xxx', params: {xxx:123...}})这种方式.这种方式导致

随机推荐