Vue深入学习系列:vuex原理解析

489 阅读6分钟

前言:vuex是Vue全家桶中对数据流整体控制的一环,也是面试题中经常问到的技术点,平时我们开发时经常使用,vuex对数据控制,更改以及为什么对Vue的强依赖的背后,又是什么原理呢,今天大家就一起探究,手写一份简单的vuex

  1. 第一部分回整个vuex的概念,安装,以及借用官方一个简单的例子,灰常熟悉的朋友,可以直接略过...OUO!
    • Vuex 是什么?

      Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension (opens new window),提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

    • 什么是“状态管理模式”?

      状态自管理应用包含以下几个部分:

      • state,驱动应用的数据源;

      • view,以声明方式将 state 映射到视图;

      • actions,响应在 view 上的用户输入导致的状态变化。

        以下是一个表示“单向数据流”理念的简单示意:

        但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

        • 多个视图依赖于同一状态。

        • 来自不同视图的行为需要变更同一状态。

        对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

        因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

        通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

        这就是 Vuex 背后的基本思想,借鉴了 Flux (opens new window)Redux (opens new window)The Elm Architecture (opens new window)。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

        但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

        • 多个视图依赖于同一状态。

        • 来自不同视图的行为需要变更同一状态。

        对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

        因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

        通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

        这就是 Vuex 背后的基本思想,借鉴了 Flux (opens new window)Redux (opens new window)The Elm Architecture (opens new window)。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。)

    • 安装

      NPM

      npm install vuex --save
      

      Yarn

      yarn add vuex
      

      vue-cli脚手架自带

      vue add vuex
      

      在一个模块化的打包系统中,必须显式地通过 Vue.use() 来安装 Vuex:

      import Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)
      
    • 如果还不熟悉vuex的操作应用的话,可以查看官方文档还有Github地址哦

  2. 安装完成后,借用一下官方的案例,使用最简单的加法案例,整个vuex共使用到4个文件,store/index.js,store/my-vuex.js,main.js以及应用的组件(Home.vue)
    • 文件目录结构

    • store/index.js

      // 一个我们最最常用的结构(熟悉的味道,熟悉的配方OUO)
      import Vue from 'vue'
      // import Vuex from 'vuex' 注释掉官方的
      import Vuex from './my-vuex'
      // 实现install
      Vue.use(Vuex)
      
      export default new Vuex.Store({
        state: {
          count: 0,
        },
        mutations: {
          setCount(state, payload) {
            state.count += payload
          },
        },
        actions: {
          asyncSetCount({ commit }, payload) {
            setTimeout(() => {
              commit('setCount', payload)
            }, 1000)
          },
        },
        gutters: {
          guttersCount(state) {
            return state.count * 2
          },
        },
      })
      
    • main.js,vue项目的打包入口

      import Vue from 'vue'
      import App from './App.vue'
      import router from './router'
      import store from './store'
      
      Vue.config.productionTip = false
      
      new Vue({
        router,
        //这个是我们自己的 new Store实例哦!!
        store,
        render: h => h(App)
      }).$mount('#app')应用部分Home.vue文件,一个简单的加法案例
      
    •   <template>
          <div class="home">
            <h3>{{ $store.state.count }}</h3>
            <h3>gutters:{{ $store.gutters.guttersCount }}</h3>
            <button @click="$store.commit('setCount', 1)">mutation</button>
            <button @click="$store.dispatch('asyncSetCount', 2)">async</button>
          </div>
        </template>
        
        <script>
        export default {
          name: "Home"
        };
        </script>
      
    • 后面就是我们的主角,my-vuex的登场,我们一步一步剖析vuex的原理,手动实现一个简单的vuex吧

  3. 手动实现vuex

    • 首先vuex是Vue的插件系统,所以实现中会有最基础的两个部分,分别为本身的Store实例,install插件的方法,我们先搭建一个基本的Vue插件的架子,其中,我们平时在使用插件时会使用Vue.use('插件')方法时会实现插件的install方法,并传入Vue的实例,以此来关联Vue的实例与插件

      // 基本的架子
      //全局定义一个Vue,用来接收install中的Vue根实例
      let Vue 
      class Store {
          constructor(options){
              this.options = options
          }
      }
      
      //Vue在使用Vue.use()方法时,会调用install方法,并将Vue实例传入
      function install(_Vue){
          Vue = _Vue
      }
      
      //在上面的示例中可以看到,store/index.js中引入Vuex,其实就是这两个导出的对象
      export default {
          Store,
          install
      }
      
    • 这时,我们的页面肯定是报错的,在页面中this并没有store这个属性,那又从哪里得到呢?main.js中其实是有我们一开始导入的Store实例,在一开始的根实例中,我们是可以借助store这个属性,那又从哪里得到呢?在main.js中其实是有我们一开始导入的Store实例,在一开始的根实例中,我们是可以借助options这个方法得到的,并且可以使用Vue的mixin混入到全局的实例中,其中我们的混入时机就放在全局的beforeCreate中挂载

      //Vue在使用Vue.use()方法时,会调用install方法,并将Vue实例传入
      function install(_Vue){
          Vue = _Vue
          //只在全局第一次初始化的时候挂载,避免子组件重复挂载
          if(this.$options.store){
              //将store挂载至Vue实例的原型上面
              Vue.prototype.$stroe = this.$options.store
          }
      }
      
    • 在上一步处理完了,将store挂载在Vue原型后,就要具体实现vuex中的属性与方法了,其中先把基本需要的搭建起来,

      class Store {
          constructor(options){
              //保存属性
              this.mutations = options.mutations
              this.actions = options.actions
          }
          
          dispatch(){}
          
          commit(){}
      }
      
    • 这个时候先从state开始处理,state是全局属性,并且再被mutation改变的时候,页面使用的位置也会做出相应的update,这时候就需要让数据响应式处理了,还记得那个一开始定义的全局Vue嘛,在被install中赋值之后,我们就利用对state进行响应式处理.响应式处理有两个思路,第一个Vue.util.defineReactive(),在上一个vue-router文章中,感兴趣的可以看一下,这次我们换一个实现的方法,俗称"借鸡生蛋",那就是我们在new一个Vue的实例,并且吧数据定义在date中,使用类似数据总线的方式,并且对state的请求,可以使用存取器的方法,代理至我们这个新的Vue实例上,并且state是允许直接修改,应该通过mutation,我们进行一下代码的实现

      //已经在install赋值后的Vue
      let Vue
      class Store {
          constructor(options){
              //保存属性
           	
              this.mutations = options.mutations
              this.actions = options.actions
              
            	this._vm = new Vue({
                  data:{
                      //加上$$符是为了不让Vue进行代理,具体可以查看官方源码解释
                      //使用options是因为正在get中拿不到this._vm,所以并未使用this.state
                      $$state:options.state
                  }
              })
          }
          
          get state(){
              //$data 大家可以console输出一下Vue就知道了
              return this._vm.$data.$$state
          }
          
          set state(){
              console.log('请通过mutation修改state的值!!')
          }
          
          dispatch(){}
          
          commit(){}
      }
      
    • 现在对方法的实现,其实平时我们使用dispatch,commit方法时,都会传入两个参数,嗲用actions,mutations中的方法,其实实现起来很简单,要注意的是,在action中经常会处理请求等异步的情况,它的this指向很混乱,所以这里需要给dispatch,commit方法this指向绑定

      let Vue
      class Store {
        constructor(options){
            //保存属性
            this.mutations = options.mutations
            this.actions = options.actions
            
            this._vm = new Vue({
                  data:{
                      //加上$$符是为了不让Vue进行代理,具体可以查看官方源码解释
                      $$state: options.state
                  }
              })
            
            this.dispatch = this.dispatch.bind(this)
            this.commit = this.commit.bind(this)
        }
        get state(){
              //$data 大家可以console输出一下Vue就知道了
              return this._vm.$data.$$state
          }
          
          set state(){
              console.log('请通过mutation修改state的值!!')
          }
        //两个参数分别为actions中的方法,你需要传入的参数
        dispatch(type,payload){
            //actions的方法中的参数,也在这里传入
            //平时我们在第一个参数中经常是{commit}的写法,其中还有很多dispatch,state等等,来进行更复杂的业务逻辑
            //平时一些复杂的请求处理都会放在action中执行,这里为了省事,我就直接把this放进去了,更深入的大家可以查看源码哦
            this.actions[type](this,payload)
        }
        //两个参数分别为mutations中的方法,你需要传入的参数
        commit(type,payload){
            //mutation就比较简单了,就是传入state,还有payload执行修改state
            this.mutation[type](this.state,payload)
        }
      }
      
    • 接下来就剩下最后一步,gutters的实现,gutter参数中,是对应的gutters对象中方法的输出,将state传入后的实现,其实也同样可以利用存取器的方案进行实现

      //粗浅的实现  
      get gutters() {
          const obj = {}
          //循环遍历后,传入整个state
          Object.keys(this.options.gutters).forEach(
            (key) => (obj[key] = this.options.gutters[key](this.state))
          )
          return obj
        }
      

      但是这个方法很粗浅,虽然可以实现需求,但是每次都执行一遍对gutters的遍历很没有效率,其实我们写gutters的时候,经常将它放在Vue的computed中使用,那么我们同样可以利用这一点,还记得那个this._vm嘛,一样可以使用computed进行对gutters使用

      let Vue
      
      class Store {
        constructor(options) {
          this.mutations = options.mutations
          this.actions = options.actions
          //先拿出gutters
       	this._wrapGutters = options.gutters
          // 定义真正的gutters
          this.gutters = {}
          //指向gutterr后的代理至this._vm的计算属性
          const computed = {}
          //防止this指向丢失
          const store = this
          Object.keys(this._wrapGutters).forEach(key=>{
              const fn = this._wrapGutters[key]
              computed[key] = function (){
                  return fn(store.state)
              }
              //我们的gutter是只读属性,并且在被调用时指向this_vm的computed属性中,可以使用Object.defineproperty实现
              Object.defineProperty(this.gutters,key,{
                  get(){
                      return () => store._vm[key]
                  }
              })
          })
          
          
          this._vm = new Vue({
            //将gutters指向到这里
            computed,
            data: {
              $$state: options.state,
            },
          })
      
          this.commit = this.commit.bind(this)
          this.dispatch = this.dispatch.bind(this)
        }
        get state() {
          return this._vm.$data.$$state
        }
        set state(v) {
          console.log('不修改')
        }
      
        // get gutters() {
        //   const obj = {}
        //   Object.keys(this.options.gutters).forEach(
        //     (key) => (obj[key] = this.options.gutters[key](this.state))
        //   )
        //   return obj
        // }
      
        commit(type, payload) {
          this.mutations[type](this.state, payload)
        }
        dispatch(type, payload) {
          this.actions[type](this, payload)
        }
      }
      

      好了gutters实现了,以上就是我们这次简单的一个vuex的手写部分,希望大家有所收获

  4. 总结:
    • Vue插件机制,需要实现install方法,install第一个参数为Vue的实例,需要全局保存

    • 在vuex将gutters中的属性保存为自身的实例属性,通过main.js中拿到的Store实例,通过Vue.mixin方法在全局混入Stroe的实例,在第一次初始化的时候混入到Vue根实例的原型中,以便各个组件中调用

    • 使用"借鸡生蛋"的方法,将store中state值,变为响应式数据,使state改变时,触发对应的update函数

    • 在处理gutters时,使用了全局Vue的computed属性,使gutter更有效率并具有缓存效果

上期文章Vue深入学习系列:vue-router的核心原理学习剖析

如果,文章中有任何侵权行为,请与我联系