vuex 基础实现

vuex使用

首先知道vuex的基本用法,才能具体实现原理。

1.创建项目

基于vue-cli 创建一个项目:

vue create vuex-project

打开项目,打开store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

实现

可以看出来,Vuex需要实现两个功能,一个是 install方法,以便支持use,还有一个是 Store类:

vuex/index.js

// 导出 install中传入的当前Vue,后续在使用Vue的时候就是直接使用的当前初始化时候传入的Vue
// export 输出的接口与绑定的值是动态绑定关系,所以后续取到的值 Vue是实时的值,这里最后取到的就是 install 中传入的 _Vue
//  更多参考 阮一峰的ES6:https://es6.ruanyifeng.com/#docs/module
export let Vue

function install (_Vue) {
  Vue = _Vue
}

class Store {
  constructor (options) {
    console.log(options)
  }
}

export default {
  install,
  Store
}

至此,vuex的基础壳子就出来了。不过后续为了维护方便,下面会将 install方法 和 Store类单独写在两个文件里。

在使用vuex的时候,还有一步,就是在main.js中挂载到Vue实例上:

import store from './store'


new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

然后在每个Vue组件实例上都可以通过this.$store拿到这个store。所以这个store需要挂载到全局。

首先想到的是通过原型prototype来挂载,但是这样会导致一个问题,假如我们的需求需要多个new Vue,在其中一个new Vue的时候添加store,另一个不添加 store:

let vm1 = new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

let vm2 = new Vue({ // 这个不需要挂载 store
  render: h => h(App)
}).$mount('#app')

但是通过prototype方式挂载store,会导致每个Vue实例上都会有 store,不满足我们的需求。Vue的做法是通过Vue.mixin,给每个组件挂载一个beforeCreate钩子,然后拿到 store 挂载到自己身上。

install 方法

// 导出 install中传入的当前Vue,后续在使用Vue的时候就是直接使用的当前初始化时候传入的Vue
// export 输出的接口与绑定的值是动态绑定关系,所以后续取到的值 Vue是实时的值,这里最后取到的就是 install 中传入的 _Vue
//  更多参考 阮一峰的ES6:https://es6.ruanyifeng.com/#docs/module
export let Vue

function install (_Vue) {
  Vue = _Vue
  Vue.mixin({
    // 这里通过 mixin 添加的这个钩子,会通过 mergeOptions 合并给每个组件实例,所以每个组件实例都会调用这个钩子
    beforeCreate () { // 这里的this 是每个组件实例
      const options = this.$options
      // 获取根组件上的实例,共享给每个组件
      if (options.store) {
        this.$store = options.store
      } else { // 如果没有 store,说明 new Vue的时候没有进行挂载,或者 是子组件调用,那就获取 父组件的 $store
        if (this.$parent && this.$parent.$store) {
          // 创建的时候 顺序 是 create根组件 -> 然后 create父组件 -> create 子组件 -> create 孙子 .....,所以当子组件执行这里时候,根组件的 $store 已经被挂到 子组件的父组件上了
          this.$store = this.$parent.$store
        }
      }
    }
  })
}

export default install

store.js

class Store {
  constructor (options) {
    console.log(options)
    const { state, mutations, actions, getters } = options
    this.state = state
  }
}
export default Store

这时候 在 App.vue中通过 $store.state 就可以拿到值了。

不过此时的state数据无法响应式。在App.vue中添加一个测试button:

<div id="app">
    姓名:{{$store.state.name}}
    年龄:{{$store.state.age}}
    <!-- getters:{{$store.getters.myAge}} -->
    <!-- 正常开发是不允许这样修改状态的,这里仅仅为了测试 -->
    <button @click="$store.state.age++">更改年龄</button>
  </div>

点击发现数据没有任何改变。官方做法是通过借助Vue来直接实现响应式(侧面也说明了vuex必须是和Vue搭配使用,不想redux不依赖react)。

store.js

import { Vue } from './install'
class Store {
  constructor (options) {
    console.log(options)
    const { state, mutations, actions, getters } = options

    this._vm = new Vue({
      data: { // $ 开头的属性不会被挂载到vm实例上,会被挂载到_data 上 。所以直接通过_vm.$$state是取不到的
        $$state: state
      }
    })
  }

  // 属性访问器
  get state () {
    return this._vm._data.$$state
  }
}
export default Store

借助 vue的响应式,此时再点按钮就可以实时更新数据了。

实现getters

使用官方版的Vuex,修改App.vue

<div id="app">
    姓名:{{$store.state.name}}
    年龄:{{$store.state.age}}
    getters:{{$store.getters.myAge}}
    getters:{{$store.getters.myAge}}
    getters:{{$store.getters.myAge}}

    <!-- 正常开发是不允许这样修改状态的,这里仅仅为了测试 -->
    <button @click="$store.state.age++">更改年龄</button>
  </div>

store/index.js中打印:

getters: {
    myAge (state) {
      console.log('getters')
      return state.age + 10
    }
  },

发现不管调用几次getters,只会打印一次,感觉就像我们的计算属性。其实getters内部就是基于计算属性来实现的。

store.js中添加下面逻辑:

const forEach = function (obj, fn) {
  Object.keys(obj).forEach((key) => {
    fn(obj[key], key)
  })
}

class Store {
  constructor (options) {
    console.log(options)
    const { state, mutations, actions, getters } = options
    this.getters = {}
    const computed = {}

    forEach(getters, (fn, key) => {
      computed[key] = () => {
        return fn(this.state)
      }

      // 每次取getter值的时候,借助_vm.computed 来取值
      Object.defineProperty(this.getters, key, {
        get: () => this._vm[key]
      })
    })

    this._vm = new Vue({
      data: { // $ 开头的属性不会被挂载到vm实例上,会被挂载到_data 上 。所以直接通过_vm.$$state是取不到的
        $$state: state
      },
      computed
    })
  }

实现mutations

mutations 就是发布订阅的思想。初始化的时候,将用户传入的 mutations 收集起来,等到调用commit函数的时候,触发收集的所有符合的mutations

store.js

添加下面代码:

 this.mutations = {}
    forEach(mutations, (fn, key) => {
      this.mutations[key] = (payload) => fn.call(this, this.state, payload)
    })
 
commit (type, payload) {
    this.mutations[type](payload)
  }

在这里插入图片描述

实现 actions

基本思路和mutations 一样。
主要代码如下:
在这里插入图片描述
然后在外面使用:

<button @click="$store.dispatch('asyncChangeName','newName')">异步修改名字</button>

actions: {
    asyncChangeName ({ commit }, val) {
      setTimeout(() => {
        commit('changeName', 'asyncName')
      }, 1000)
    }
  },

至此 一个基础版的vuex就实现了,后面会继续完成module等功能。

扩展

export

export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

上面代码输出变量foo,值为bar,500 毫秒之后变成baz。

这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。

摘自 : 阮一峰老师的ES6 : https://es6.ruanyifeng.com/#docs/module

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值