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


Posted in Javascript onMarch 01, 2019

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

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

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

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

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

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

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

使用vuex

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

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

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实现及简略解析(小结)

到这里我们使用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({...})的实例。如下图

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

通过层层向上寻找,让每个组件都挂上了一个$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的内部代码的工作流程了,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>

最后查看结果。

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

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

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

Javascript 相关文章推荐
JavaScript setTimeout和setInterval的使用方法 说明
Mar 25 Javascript
juqery 学习之三 选择器 可见性 元素属性
Nov 25 Javascript
javascript操作excel生成报表全攻略
May 04 Javascript
table insertRow、deleteRow定义和用法总结
May 14 Javascript
用svg制作富有动态的tooltip
Jul 17 Javascript
深入解析JavaScript中的数字对象与字符串对象
Oct 21 Javascript
Bootstrap每天必学之日期控制
Mar 07 Javascript
Vue.js快速入门实例教程
Oct 15 Javascript
vue2.0中vue-cli实现全选、单选计算总价格的实例代码
Jul 18 Javascript
js中apply()和call()的区别与用法实例分析
Aug 14 Javascript
vue组件之间数据传递的方法实例分析
Feb 12 Javascript
OpenLayer学习之自定义测量控件
Sep 28 Javascript
简单两步使用node发送qq邮件的方法
Mar 01 #Javascript
Vue实现类似Spring官网图片滑动效果方法
Mar 01 #Javascript
[原创]微信小程序获取网络类型的方法示例
Mar 01 #Javascript
使用JavaScript解析URL的方法示例
Mar 01 #Javascript
可能被忽略的一些JavaScript数组方法细节
Feb 28 #Javascript
Vue插件从封装到发布的完整步骤记录
Feb 28 #Javascript
Javascript删除数组里的某个元素
Feb 28 #Javascript
You might like
Window下PHP三种运行方式图文详解
2013/06/11 PHP
PHP编程快速实现数组去重的方法详解
2017/07/22 PHP
PHP实现将几张照片拼接到一起的合成图片功能【便于整体打印输出】
2017/11/14 PHP
ThinkPHP 3使用OSS的方法
2018/07/19 PHP
Thinkphp 框架扩展之行为扩展原理与实现方法分析
2020/04/23 PHP
javascript css float属性的特殊写法
2008/11/13 Javascript
jquery tablesorter.js 支持中文表格排序改进
2009/12/09 Javascript
特殊情况下如何获取span里面的值
2014/05/20 Javascript
Javascript基础教程之数据类型 (布尔型 Boolean)
2015/01/18 Javascript
js编写贪吃蛇的小游戏
2020/08/24 Javascript
JS实现输入框提示文字点击时消失效果
2016/07/19 Javascript
教你JS中的运算符乘方、开方及变量格式转换
2016/08/09 Javascript
js原生之焦点图转换加定时器实例
2016/12/12 Javascript
jQuery Easyui datagrid行内实现【添加】、【编辑】、【上移】、【下移】
2016/12/19 Javascript
vue.js国际化 vue-i18n插件的使用详解
2017/07/07 Javascript
vue Render中slots的使用的实例代码
2017/07/19 Javascript
jquery ztree实现右键收藏功能
2017/11/20 jQuery
vue 之 css module的使用方法
2018/12/04 Javascript
详解解决小程序中webview页面多层history返回问题
2019/08/20 Javascript
JS实现简单省市二级联动
2019/11/27 Javascript
VUE项目axios请求头更改Content-Type操作
2020/07/24 Javascript
vue-cli单页面预渲染seo-prerender-spa-plugin操作
2020/08/10 Javascript
JavaScript函数柯里化实现原理及过程
2020/12/02 Javascript
Python 26进制计算实现方法
2015/05/28 Python
python中计算一个列表中连续相同的元素个数方法
2018/06/29 Python
使用python Fabric动态修改远程机器hosts的方法
2018/10/26 Python
Python3.5文件读与写操作经典实例详解
2019/05/01 Python
tensorflow 实现数据类型转换
2020/02/17 Python
美国电视购物HSN官网:HSN
2016/09/07 全球购物
美国单身专业人士在线约会网站:EliteSingles
2019/03/19 全球购物
Book Depository欧盟:一家领先的国际图书零售商
2019/05/21 全球购物
宣传策划类求职信范文
2014/01/31 职场文书
联谊会开场白
2015/06/01 职场文书
小学校园广播稿
2015/08/18 职场文书
2016年教师党员承诺书范文
2016/03/24 职场文书
使用CSS实现百叶窗效果示例代码
2023/05/07 HTML / CSS