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 特殊字符
Apr 05 Javascript
JavaScript Event学习第七章 事件属性
Feb 07 Javascript
把jquery 的dialog和ztree结合实现步骤
Aug 02 Javascript
JS常用函数使用指南
Nov 23 Javascript
Javascript设计模式理论与编程实战之简单工厂模式
Nov 03 Javascript
JavaScript中的原始值和复杂值
Jan 07 Javascript
第九章之路径分页标签与徽章组件
Apr 25 Javascript
详解使用vue-router进行页面切换时滚动条位置与滚动监听事件
Mar 08 Javascript
使用原生js封装的ajax实例(兼容jsonp)
Oct 12 Javascript
在ES5与ES6环境下处理函数默认参数的实现方法
May 13 Javascript
JS实现网页时钟特效
Mar 25 Javascript
学习 Vue.js 遇到的那些坑
Feb 02 Vue.js
简单两步使用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
深入解析PHP的Yii框架中的event事件机制
2016/03/17 PHP
PHP实现的Redis多库选择功能单例类
2017/07/27 PHP
PHP基于phpqrcode类生成二维码的方法示例详解
2020/08/07 PHP
初学JavaScript_03(ExtJs Grid的简单使用)
2008/10/02 Javascript
Jquery CheckBox全选方法代码附js checkbox全选反选代码
2010/06/09 Javascript
30个精美的jQuery幻灯片效果插件和教程
2011/08/23 Javascript
thinkphp 表名 大小写 窍门
2015/02/01 Javascript
js+jquery常用知识点汇总
2015/03/03 Javascript
JS函数的定义与调用方法推荐
2016/05/12 Javascript
Windows系统下安装Node.js的步骤图文详解
2016/11/15 Javascript
javascript实现无法关闭的弹框
2016/11/27 Javascript
使用Promise链式调用解决多个异步回调的问题
2017/01/15 Javascript
微信小程序-滚动消息通知的实例代码
2017/08/03 Javascript
JS图片预加载三种实现方法解析
2020/05/08 Javascript
[08:08]2014DOTA2国际邀请赛中国区预选赛精彩TOPPLAY
2014/06/25 DOTA
Python基于回溯法子集树模板解决马踏棋盘问题示例
2017/09/11 Python
python爬虫获取京东手机图片的图文教程
2017/12/29 Python
Python 实现加密过的PDF文件转WORD格式
2020/02/04 Python
python tkinter实现下载进度条及抖音视频去水印原理
2021/02/07 Python
Dr. Martens马汀博士澳大利亚官网:马丁靴鼻祖
2019/07/02 全球购物
Yves Rocher捷克官方网站:植物化妆品的创造者
2019/07/31 全球购物
海蓝之谜英国官网:La Mer英国
2020/01/15 全球购物
Java的for语句中break, continue和return的区别
2013/12/19 面试题
什么是测试驱动开发(TDD)
2012/02/15 面试题
体育系毕业生自荐信
2014/06/28 职场文书
优秀本科毕业生自荐信
2014/07/04 职场文书
食品安全承诺书范文
2014/08/29 职场文书
农村党支部书记党群众路线四风问题整改措施
2014/09/26 职场文书
公安纪律作风整顿剖析材料
2014/10/10 职场文书
狼牙山五壮士观后感
2015/06/09 职场文书
傲慢与偏见电影观后感
2015/06/10 职场文书
公安干警正风肃纪心得体会
2016/01/15 职场文书
sql查询结果列拼接成逗号分隔的字符串方法
2021/05/25 SQL Server
pytorch fine-tune 预训练的模型操作
2021/06/03 Python
MySQL笔记 —SQL运算符
2022/01/18 MySQL
Java设计模式之享元模式示例详解
2022/03/03 Java/Android