一杯茶的时间重温vuex核心源码

370 阅读1分钟

结合使用方式,重温核心源码,你也可以实现一个简易的vuex。

  1. 我们是如何使用vuex的? 首先,在main.js引入配置好的store,
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

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

App.vue 中使用state中的值,

<template>
  <div id="app">
    my age {{age}}
    <br>
    your age {{getAge}}
    <br>
    <button @click="$store.state.age += 100">直接修改状态</button>
    <button @click="changeAge(2)">同步更新状态</button>
    <br>
    <button @click="$store.dispatch('changeAge', 3)">异步更新状态</button>
  </div>
</template>
import {mapState, mapGetters, mapMutations, mapAtions} from './vuex'

export default {
  name: 'App',
  computed: {
    ...mapState(['age']),
    ...mapGetters(['getAge'])
  },
  created() {
    console.log('app=', this.$store)
  },
  methods: {
    ...mapMutations(['changeAge']),
    // ...mapAtions(['changeAge'])
    // changeAge(payload) {
    //   this.$store.commit('changeAge', payload)
    // }
  }
}

然后编写自己的store.js, vuex是和Vue强绑定的,利用Vue的插件机制,在Vue中先安装vuex;

import Vue from 'vue'
// import Vuex from 'vuex'
import Vuex from '../vuex'

Vue.use(Vuex)

// 内部会创建一个vue实例,用于跨组件通信
const store = new Vuex.Store({
  // 组件状态  ==> new Vue的data
  state: {
    //..
  },
  // 获取计算属性,当依赖的值变化时 会重新执行
  getters: {
    //..
  },
  // new Vue中的method,唯一可以改状态的方法
  mutations: {
    //..
  },
  // 如果改变的异步的,需要在actions中发起请求,
  actions: {
    //..
  },
  modules: {
    //..
  }
})

export default store

2.实现vuex

首先从入口文件开始,vuex/index.js作为入口文件,作用就是整合;

import { Store, install } from './store'
import {mapState, mapGetters, mapMutations, mapAtions} from './helper'

export default {
  Store,
  install,
  mapState,
  mapGetters,
  mapMutations,
  mapAtions
}

然后来到核心文件vuex/store.js,先看主体结构,

import ModuleCollections from './module/module-collections'
let Vue;
class Store {
    this._modules = new ModuleCollections(options)
    //...
    installModule(this, state, [], this._modules.root)
    //...
    resetStoreVm(this, state)
    //...
}
const install = (_Vue) => {
  Vue = _Vue;
  applyMixin(Vue);
}
export {
  Store,
  install
}

vuex的store是如何挂载注入到组件中呢?

利用vue的插件机制,使用Vue.use(vuex)时,会调用vuex的install方法,装载vuex;applyMixin方法使用vue混入机制,在vue的生命周期beforeCreate钩子函数前混入vuexInit方法;

组件的创建过程是 先父后子,最后是给每个组件都加了$store这个属性,指向同一个属性,所以最后状态是公用的;

和vue-router不一样(vue-router 是把属性定义到根实例上,所有组件都能找这个根,通过根实例获取这个属性)。

const applyMixin = (Vue) => {
  Vue.mixin({
    beforeCreate: VuexInit
  })
}

function VuexInit() {
  // 获取当前Vue实例的选项
  const options = this.$options
  // 只有根实例上才有store属性
  if(options.store) { // 根组件
    this.$store = options.store
  } else if(options.parent && options.parent.$store){ // 子组件 有parent时
    this.$store = options.parent.$store
  }
}

再看初始化时,

1)我们先实例化ModuleCollections,格式化用户传入的参数成树形结构,更直观更好操作;

看下代码vuex/module/module-collections.js,通过递归注册模块,收集模块转换成一棵树;

import {forEach} from '../utils.js'
import Module from './module'

export default class ModuleCollection {
  constructor(options) {
    // 注册模块
    this.register([], options)
  }
  register(path, rootModule) {
    let newModule = new Module(rootModule)
    rootModule.newModule = newModule  // 给当前要注册的模块上 做一个映射
    if(path.length == 0) { // 根模块
      this.root = newModule
    } else {
      let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo.getChild(current)
      }, this.root)
      parent.addChild(path[path.length - 1], newModule)
    }
    if(rootModule.modules) {  // 如果有modules 说明有子模块
      forEach(rootModule.modules, (module, moduleName) => {
        this.register([...path, moduleName], module)
      })
    }
  }
  // 获取命名空间
  getNamespace(path) {
    let root = this.root  // [b,c]
    return path.reduce((namespace, key) => {
      root = root.getChild(key)
      // namespaced 如果有值,则不停的拼接
      return namespace + (root.namespaced ? key + '/' : '')
    }, '')
  }
}

Module类定义了模块的一些属性和方法,在vuex/module/module.js里;

import {forEach} from '../utils.js'

export default class Module {
  constructor(rootModule) {
    this._rawModule = rootModule;
    this._children = {};
    this.state = rootModule.state;
  }
  get namespaced() {
    return this._rawModule.namespaced
  }
  getChild(key) {
    return this._children[key]
  }
  addChild(key, module) {
    this._children[key] = module;
  }
  forEachMutation(fn) {
    if(this._rawModule.mutations) {
      forEach(this._rawModule.mutations, fn)
    }
  }
  forEachAction(fn) {
    if(this._rawModule.actions) {
      forEach(this._rawModule.actions, fn)
    }
  }
  forEachGetter(fn) {
    if(this._rawModule.getters) {
      forEach(this._rawModule.getters, fn)
    }
  }
  forEachChild(fn) {
    forEach(this._children, fn)
  }
}

2)执行 installModule 方法,注册模块,如果是子模块,需要将子模块的状态定义到根模块;

function installModule(store, rootState, path, module) {
  // 注册事件时,需要注册到对应的命名空间,path就是所有的路径,根据path算出一个空间里
  let namespace = store._modules.getNamespace(path)
  console.log('namepase', namespace)
  // 如果是子模块 需要将子模块的状态定义到根模块
  if(path.length > 0) {
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current]
    }, rootState)
    store._withCommitting(() => {
      // Vue.set区分是否是响应式数据
      Vue.set(parent, path[path.length - 1], module.state)
    })
  }

  module.forEachMutation((mutation, type) => {
    store._mutations[namespace + type] = store._mutations[namespace + type] || []
    store._mutations[namespace + type].push((payload) => {
      // mutation.call(store, module.state, payload)
      // 只有通过mutation更改状态,断言才能通过
      store._withCommitting(() => {
        mutation.call(store, getState(store, path), payload)
      })
      // 执行订阅的事件
      store._subscribers.forEach(sub => sub({mutation, type}, store.state))
    })
  })
  module.forEachAction((action, type) => {
    store._actions[namespace + type] = store._actions[namespace + type] || []
    store._actions[namespace + type].push((payload) => {
      action.call(store, store, payload)
    })
  })
  module.forEachGetter((getter, type) => {
    // 如果getters 重名会覆盖,所有模块的getter都会定义到根模块上
    store._wrappedGetters[namespace + type] = function() {
      return getter(getState(store, path))
    }
  })
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

调用 forEachMutation 方法,将用户传入的mutations转换,存放到存放所有模块的mutations的_mutations对象中,键是'namespace + type',以此区分不同模块下的同名mutation,值是mutation对应的方法的数组,在用户执行commit时,依次执行对应的方法;

commit = (type, payload) => {
    this._mutations[type].forEach(fn => fn(payload))
  }

同理,执行forEachAction,将用户传入的actions转换,存放到存放所有模块的actions的_actions对象中。

不同之处在于:mutation被包裹了一层_withCommitting,只有通过mutation更改状态,断言才能通过;

// mutations
store._mutations[namespace + type].push((payload) => {
  store._withCommitting(() => {
    mutation.call(store, getState(store, path), payload)
  })
})

//actions
store._actions[namespace + type].push((payload) => {
  action.call(store, store, payload)
}) 

// 切片,判断是否是mutation修改状态
_withCommitting(fn) {
    let committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

//在resetStoreVm里
store._vm.$watch(() => store._vm._data.$$state, () => {
    console.assert(store._committing, '在mutation之外更改了状态')
 }, {deep: true, sync: true})

3)执行resetStoreVm(this, state),将用户传入的数据定义在vue的实例上,产生一个单独的vue实例进行通信,这个就是vuex核心

function resetStoreVm(store, state) {
  const wrappedGetters = store._wrappedGetters
  let oldVm = store._vm
  let computed = {}  // 通过computed实现缓存效果
  store.getters = {}
  // 让getters 定义在store上
  forEach(wrappedGetters, (fn, key) => {
    computed[key] = function() {
      return fn()
    }
    // 重写 get 方法
    // store.getters.xx 其实是访问了store._vm[xx],其中添加 computed 属性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })
  // 创建Vue实例来保存state,同时让state变成响应式
  // store._vm._data.$$state = store.state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  if(store.strict) {
    // 只有状态变化 会立即执行
    store._vm.$watch(() => store._vm._data.$$state, () => {
      console.assert(store._committing, '在mutation之外更改了状态')
    }, {deep: true, sync: true})
  }
  if(oldVm) {
    Vue.nextTick(() => {
      oldVm.$destroy()
    })
  }
}

最后对照vuex/store.js完整代码看一下:

import ModuleCollections from './module/module-collections'
let Vue;

function installModule(store, rootState, path, module) {
  // 注册事件时,需要注册到对应的命名空间,path就是所有的路径,根据path算出一个空间里
  let namespace = store._modules.getNamespace(path)
  console.log('namepase', namespace)
  // 如果是子模块 需要将子模块的状态定义到根模块
  if(path.length > 0) {
    let parent = path.slice(0, -1).reduce((memo, current) => {
      return memo[current]
    }, rootState)
    store._withCommitting(() => {
      // Vue.set区分是否是响应式数据
      Vue.set(parent, path[path.length - 1], module.state)
    })
  }

  module.forEachMutation((mutation, type) => {
    store._mutations[namespace + type] = store._mutations[namespace + type] || []
    store._mutations[namespace + type].push((payload) => {
      // mutation.call(store, module.state, payload)
      // 只有通过mutation更改状态,断言才能通过
      store._withCommitting(() => {
        mutation.call(store, getState(store, path), payload)
      })
      // 执行订阅的事件
      store._subscribers.forEach(sub => sub({mutation, type}, store.state))
    })
  })
  module.forEachAction((action, type) => {
    store._actions[namespace + type] = store._actions[namespace + type] || []
    store._actions[namespace + type].push((payload) => {
      action.call(store, store, payload)
    })
  })
  module.forEachGetter((getter, type) => {
    // 如果getters 重名会覆盖,所有模块的getter都会定义到根模块上
    store._wrappedGetters[namespace + type] = function() {
      return getter(getState(store, path))
    }
  })
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

function resetStoreVm(store, state) {
  const wrappedGetters = store._wrappedGetters
  let oldVm = store._vm
  let computed = {}  // 通过computed实现缓存效果
  store.getters = {}
  // 让getters 定义在store上
  forEach(wrappedGetters, (fn, key) => {
    computed[key] = function() {
      return fn()
    }
    // 重写 get 方法
    // store.getters.xx 其实是访问了store._vm[xx],其中添加 computed 属性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })
  // 将用户传入的数据定义在vue的实例上 (这个就是vuex核心)产生一个单独的vue实例进行通信
  // 创建Vue实例来保存state,同时让state变成响应式
  // store._vm._data.$$state = store.state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  if(store.strict) {
    // 只有状态变化 会立即执行
    store._vm.$watch(() => store._vm._data.$$state, () => {
      console.assert(store._committing, '在mutation之外更改了状态')
    }, {deep: true, sync: true})
  }
  if(oldVm) {
    Vue.nextTick(() => {
      oldVm.$destroy()
    })
  }
}

// 通过路径 获得最新的state
function getState(store, path) {
  return path.reduce((newState, current) => {
    return newState[current]
  }, store.state)
}

const applyMixin = (Vue) => {
  Vue.mixin({
    beforeCreate: VuexInit
  })
}

// 组件的创建过程是 先父后子,最后是给每个组件都加了$store这个属性,指向同一个属性,所以最后状态是公用的
// 和vue-router不一样(vue-router 是把属性定义到根实例上,所有组件都能找这个根,通过根实例获取这个属性)
function VuexInit() {
  // 获取当前Vue实例的选项
  const options = this.$options
  // 只有根实例上才有store属性
  if(options.store) { // 根组件
    this.$store = options.store
  } else if(options.parent && options.parent.$store){ // 子组件 有parent时
    this.$store = options.parent.$store
  }
}

class Store {
  constructor(options) {
    // 格式化用户传入的参数,成树形结构,更直观 更好操作
    // 收集模块转换成一棵树
    this._modules = new ModuleCollections(options)
    console.log(this._modules)
    // 安装模块 将模块上的属性 定义到store中
    let state = this._modules.root.state

    this._mutations = {}  // 存放所有模块的
    this._actions = {}   // 存放所有模块的
    this._wrappedGetters = {}  // 存放所有模块的

    this._subscribers = []

    this.strict = options.strict // 严格模式
    this._committing = false // 同步的watcher

    // console.log('this._mutations', this._mutations);
    // console.log('this._actions', this._actions);
    // console.log('this._wrappedGetters', this._wrappedGetters);
    installModule(this, state, [], this._modules.root)

    // 将状态放到vue实例中
    resetStoreVm(this, state)
    // 插件的实现
    options.plugins.forEach(plugin => plugin(this))
  }
  // 切片,判断是否是mutation修改状态
  _withCommitting(fn) {
    let committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

  subscribe(fn) {
    // 先存
    this._subscribers.push(fn)
    // 状态变化时(调用commit) 执行
  }

  replaceState(newState) { // 用最新的状态替换
    this._withCommitting(() => {
      this._vm._data.$$state = newState
    })
  }

  commit = (type, payload) => {
    this._mutations[type].forEach(fn => fn(payload))
  }

  dispatch = (type, payload) => {
    this._actions[type].forEach(fn => fn(payload))
  }

  // 类的属性访问器,用户去实例上获取state属性时,执行此方法
  get state() {
    return this._vm._data.$$state
  }

  registerModule(path, rawModule) {
    if(typeof type === 'string') path = [path]
    // 模块注册
    this._modules.register(path, rawModule)
    // 模块安装 动态将状态新增上去
    installModule(this, this.state, path, rawModule.newModule)
    // 重新定义getters
    resetStoreVm(this, this.state)
  }
}

const install = (_Vue) => {
  Vue = _Vue;
  applyMixin(Vue);
}

export {
  Store,
  install
}
  1. 总结 1)vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中;

2)Vuex的getters则是借助vue的计算属性computed实现数据实时监听;

3)mutatons类似new Vue中的method,是唯一可以改状态的方法;

4)如果改变的异步的,需要在actions中发起请求;

5)默认模块没有作用域,状态不要和模块重名,如果增加namespaced,会将这个模块的属性都封装到这个作用域下;

6)默认会找当前模块上是否有namespace, 并且会将父级的一同算上,计算成命名空间;