vuex实现及简略解析

栏目: JavaScript · 发布时间: 5年前

内容简介:大家都知道通过官方文档提供的流程图我们知道,

大家都知道 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://blog.naice.me/article...

源码地址: https://github.com/naihe138/w...


以上所述就是小编给大家介绍的《vuex实现及简略解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web前端开发最佳实践

Web前端开发最佳实践

党建 / 机械工业出版社 / 2015-1 / 59.00元

本书贴近Web前端标准来介绍前端开发相关最佳实践,目的在于让前端开发工程师提高编写代码的质量,重视代码的可维护性和执行性能,让初级工程师从入门开始就养成一个良好的编码习惯。本书总共分五个部分13章,第一部分包括第1章和第2章,介绍前端开发的基本范畴和现状,并综合介绍前端开发的一些最佳实践;第二部分为第3-5章,讲解HTML相关的最佳实践,并简单介绍HTML5中新标签的使用;第三部分为第6-8章,介......一起来看看 《Web前端开发最佳实践》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具