vue-vuex原理分析

519 阅读5分钟

vuex是vue的状态管理工具,便于状态共享和传递

vuex的使用

创建store
  • Vue.use使用Vuex插件,默认调用Vuex的install方法
  • 创建store实例,传入参数options
  • state:单一的数据源,响应式
  • getters:计算属性
  • mutations:同步改变state
  • actions:异步改变state
// store.js
import Vuex from './vuex'

Vue.use(Vuex)
let store = new Vuex.Store({
  state: { age: 10 },
  getters: {
    myAge(state) {
      return state.age + 20
    }
  },
  mutations: {
    syncChange(state, payload) {
      setTimeout(() => {
        state.age += payload
      }, 1000)
    }
  },
  actions: {
    asyncChange({commit}, payload) {
      setTimeout(() => {
        commit('syncChange', payload)
      }, 1000)
    } 
  }
})
注入:在main.js中注入store
new Vue({
  name: "root",
  store,
  render: h => h(App)
}).$mount('#app')

所有组件添加$store属性原理

  • 当执行Vue.use时,默认执行插件的install方法
  • install方法:借助Vue.mixin和beforeCreate为所有组件提供$store属性获取store
const install = (_Vue) =>{//传递_Vue的目的:在写vue插件的时候,无需安装新的vue,避免版本不同出现问题
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) { // 说明是根实例
        this.$store = this.$options.store
      } else { // 组件的创建过程是先父后子,通过将$store自上而下赋值,实现所有子组件都有$store
        this.$store = this.$parent && this.$parent.$store
      }
    },
  }); 
}

Store实现

  • 实现state响应式:通过new Vue创建vue实例,对store中的数据进行劫持和依赖收集;通过get方法返回实例上的state
  • getters原理:在store实例上定义属性getters;遍历用户定义的getters对象,通过Object.defineProperty进行劫持,传入store上的state
  • mutations同步更新状态(发布订阅+函数劫持):订阅---在store实例上定义属性mutations为一个对象; 遍历用户定义的mutations,进行函数劫持,传入store.state和其他参数;发布---当执行commit方法时执行指定的方法
  • actions异步更新状态:原理同上,当执行dispatch时执行
let forEach = (obj, callback) => {
  Object.keys(obj).forEach(key => {
    callback(key, obj[key])
  })
};

class Store{
  constructor(options) {
    let {state, getters, mutations, actions} = options;
    // new Vue实现state的响应式
    this.vm = new Vue({ 
      data: { state} 
    });
    // getters通过Object.defineProperty进行劫持
    this.getters = {}
    forEach(getters, (getterName, value) => {
      Object.defineProperty(this.getters,getterName, {
        get: () => {
          return value(this.state)
        }
      })
    })
    // mutations:发布订阅+函数劫持
    this.mutations = {}
    forEach(mutations, (mutationName, value) => {
      this.mutations[mutationName] = (payload) => {
        value(this.state, payload)
      }
    })
    // actions:发布订阅+函数劫持
    this.actions = {}
    forEach(actions, (actionName, value) => {
      this.actions[actionName] = (payload) => { 
        value(this, payload);
      }
    })
  }
  dispatch = (actionName, payload) => { 
    this.actions[actionName](payload)
  }
  commit = (mutationName, payload) => { // 2.3.2 触发mutation--发布
    this.mutations[mutationName](payload)
  }
  // 返回state
  get state() {
    return this.vm.state
  }
}

模块的收集和安装

vuex中modules,用于定义指定模块的属性和方法

模块收集
  • 将模块上的数据格式化
  • 顶层的模块内容保存的this.root上;其他保存在父级的_children上
// 格式化结果:
let root = {
  _raw: rootModule,
  state: rootModule.state,
  _children: {
    a: {
      _raw: aModule,
      state: aModule.state,
      _children: {}
    },
  }
}
// 实现
class ModuleCollection{
  constructor(options) {
    this.options = options
    this.register([], options)
  }
  // 例如:options: {modules: {a: {modules: { a1: {}, a2: {} }}, b: {}}}
  register = (path, rootModule) => {
    let rawModule = { // 格式化后的模块内容
      _raw: rootModule,
      state: rootModule.state,
      namespace: rootModule.namespace,
      _children: {}
    }
    rootModule.rawModule = rawModule; // 双向记录
    if (!this.root) {
      this.root = rawModule
    } else { // 通过reduce定位父级模块
        // 如果是第二层:[a],parentModule是this.root
        // 如果是第三层: [a, a1],parentModule是this.root._children.a对应的对象
      let parentModule = path.slice(0, -1).reduce((root, current) => { 
        return root._children[current]
      }, this.root)
      parentModule._children[path[path.length - 1]] = rawModule
    }
    if (rootModule.modules) {
        // 如果是第二层:modules: {a: {modules: { a1: {}, a2: {} }}, b: {}}
        // 如果是第三层:modules: { a1: {}, a2: {} }
      forEach(rootModule.modules, (moduleName, module) => {
        // 如果是第二层:path--module: [a]-- {modules: { a1: {}, a2: {} }}
        // 如果是第三层:path--module: [a, a1]--{}
        this.register(path.concat(moduleName), module)
      })
    }
  }
}
模块安装-state、getters、mutations、actions
  • 模块安装,其实是将所有模块中的state、getters、mutations、actions等属性和方法绑定在store上,用于改变状态和视图刷新
  • namespace:用于调用指定空间的属性和方法
  • 安装state:根据path找到父级state,通过Vue.set在父级state上设置{key为模块名:value为当前模块的state}实现数据的响应式
  • 安装getters:注意避免重名
  • 安装mutations:根据mutationName进行数组存放
  • 安装actions:根据actionName进行数组存放
  • getState:返回store上对应path的state
function installModule(store, rootState, path, rawModule) { 
  // 如果有命名空间,定义命名空间前缀
  let root = store.modules.root;
  let namespace = path.reduce((str, current) => {
    root = root._children[current];
    return str = str + (root._raw.namespace ? current +
    '/' : '')
  }, '');
  // 安装state
  if (path.length > 0) {
    let parentState = path.slice(0, -1).reduce((root, current) => { 
      return rootState[current]
    }, rootState)
    Vue.set(parentState, path[path.length - 1], rawModule.state)
  }
  //安装getters:避免重名
  let getters = rawModule._raw.getters;
  if (getters) {
    forEach(getters, (getterName, value) => {
      if (!store.getters[namespace+getterName]) {
        Object.defineProperty(store.getters, namespace+getterName, {
          get: () => {
            return value(getState(store, path))
          }
        })
      }
    })
  }
  // 安装mutations:同名的mutation放在一个数组中,挂载store上
  let mutations = rawModule._raw.mutations;
  if (mutations) {
    forEach(mutations, (mutationName, value) => {
      let arr = store.mutations[namespace+mutationName] || (store.mutations[namespace+mutationName] = [])
      arr.push((payload) => {
        value(getState(store, path), payload)
      })
    })
  }
  // 安装actions:同名的action放在一个数组中,挂载store上
  let actions = rawModule._raw.actions;
  if (actions) {
    forEach(actions, (actionName, value) => {
      let arr = store.actions[namespace+actionName] || (store.actions[namespace+actionName] = [])
      arr.push((payload) => {
        value(store, payload)
      })
    })
  }
  
  // 递归安装
  if (rawModule._children) {
    forEach(rawModule._children, (moduleName, rawModule) => {
      installModule(store, rootState, path.concat(moduleName), rawModule)
    })
  }
}

function getState(store, path) {
  let local = path.reduce((newState, cur) => {
    return newState[cur]
  }, store.state)
  return local
}
插件的执行原理-发布订阅
  • 在创建store实例时,循环插件执行,传递store实现插件的初始化
  • 插件初始化时,会执行store的subscribe方法,它将订阅插件的具体执行逻辑
  • 当触发mutation改变state时,会触发插件的执行逻辑;所以,在安装模块中的mutation时,进行发布
function logger(store) { // logger插件
  console.log(store, 'store');
  let preState = JSON.stringify(store.state);
  store.subscribe((mutation, newState) => {
    console.log(preState);
    console.log(mutation);
    console.log(JSON.stringify(newState));
    preState = JSON.stringify(newState)
  })
}
---------------------------------------
class Store{
  constructor(options) {
    ...
    this.subs = [];
    let {state, plugins, strict} = options;
    plugins.forEach(plugin => plugin(this)) // 插件初始化
    ...
  }
  subscribe = (fn) => { // 插件订阅
    this.subs.push(fn)
  }
  ...
}
function installModule(store, rootState, path, rawModule) {
    ...
    let mutations = rawModule._raw.mutations;
    if (mutations) {
        forEach(mutations, (mutationName, value) => {
          let arr = store.mutations[namespace+mutationName] || (store.mutations[namespace+mutationName] = [])
          arr.push((payload) => {
            value(getState(store, path), payload);
            // 插件发布
            store.subs.forEach(sub => sub({type: namespace+mutationName, payload:payload}, store.state)) 
          })
        })
    }
    ...
}

插件数据持久化persist & store.replaceState

  • replaceState:新数据替换store中的老数据
// persist
function persist(store) {
  let local = localStorage.getItem('VUEX:state');
  if(local) {
    store.replaceState(JSON.parse(local))
  }
  store.subscribe((mutation, newState) => {
    localStorage.setItem('VUEX:state', JSON.stringify(store.state))
  })
}

class Store{
    ...
    replaceState = (newState) => {
        this.vm.state = newState
    }
  ...
}

mutation的异步监听

  • 重写commit为_withCommit(函数劫持):定义变量_committing,用于区分mutation是否为异步;所有改变状态的函数都需要_withCommit进行劫持
  • 严格模式下,通过vm.$watch监听store中的state;
  • 当mutation为异步时,this._committing为false,导致console.assert在控制台输出
class Store{
  constructor(options) {
    ...
    this._committing = false
    if (this.strict) {
      this.vm.$watch(() => {
        return this.vm.state
      }, function() {
        console.assert(this._committing,'不能异步调用')
      }, {deep: true, sync: true})
    }
    ...
  }
  ···
  _withCommit = (fn) => {
    const committing = this._committing;
    this._committing = true; 
    fn();
    this._committing = committing // 如果是同步,this._committing为true,如果是异步,则为false
  }
  
  commit = (mutationName, payload) => { // 重写commit
    this._withCommit(() => {
      this.mutations[mutationName].forEach(fn => fn(payload))
    })
  }
  
  replaceState = (newState) => {
    this._withCommit(() => {
      this.vm.state = newState
    })
  }
}