侃侃VUEX实现

简介: 「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」

「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战


前言

提及vue全家桶的时候不可忽视的就是vuex,而在我们的开发中,真正需要使用vuex的项目可能不多,至少对于我是这样的。但是这也不妨碍我们去了解下其实现原理。


原理简介

我们先来简单看看其原理



  1. 通过mixin在beforeCreate注入store属性
Vue.mixin({ beforeCreate: vuexInit })
复制代码
function vuexInit () {
  const options = this.$options
  // store injection
  if (options.store) {
    // 根实例直接使用store
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    // 组件实例通过parent向上引用
    // 这样每个组件都会有store属性并且指向同一个Store实例
    this.$store = options.parent.$store
  }
}
复制代码

  1. 响应式数据实现的原理其实就是创建vue实例
store._vm = new Vue({
  data: {
    $$state: state
  },
  // getters
  computed
})
复制代码


访问state实际是访问vue实例数据

get state () {
  return this._vm._data.$$state
}
复制代码


之前有写过一篇文章组件通信BUS模式及原理分析了BUS模式的实现原理,现在一看和vuex的响应式实现有相似之处,都创建一个vue实例。


  1. 单向数据流


我们知道有个参数strict来控制是否可以在commit之外修改state,主要是为了控制数据的单向流动。其实现原理主要是watcher加私有属性判断。


在初始化的时候

// enable strict mode for new vm
if (store.strict) {
  enableStrictMode(store)
}
复制代码


实际就是通过vm实例创建watcher实例来监听state数据的修改

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      // 这边还会进行_committing的判断
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
    // deep为true表示深度监听
  }, { deep: true, sync: true })
}
复制代码


在commit函数中实际会执行_withCommit

this._withCommit(() => {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})
复制代码


而_withCommit主要就是暂时性修改_committing达到修改state不抛出警告,这样就解释了为什么仅仅可以在commit中修改数据。


_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
复制代码

目录结构


我们从目录结构开始,来简单分析下其实现逻辑。

│  helpers.js // 辅助函数
│  index.cjs.js // commonJS入口
│  index.js // 入口
│  index.mjs // esmodule入口
│  mixin.js // mixin store实现
│  store.js // store类实现
│  util.js // 工具函数
├─module
│      module-collection.js // 递归处理module
│      module.js // moudule类对于store配置的module
└─plugins // 插件实现
      devtool.js
      logger.js
复制代码

相对于我们分析的vue和vue-router结构,vuex结构简单许多


入口


我们来看看入口indexJS,就是进行模块化导入导出,定义vuex输出

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}
复制代码

安装


vue插件都是通过install方法实现安装的,我们来看看vuex中的install实现

export function install (_Vue) {
  // 防止重复安装
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  // 其实主要是applyMixin
  applyMixin(Vue)
}
复制代码


applyMixin的重点则在于利用Vue.mixin在beforeCreate中注入store

// applyMixin
Vue.mixin({ beforeCreate: vuexInit })
复制代码
function vuexInit () {
  const options = this.$options
  // store injection
  if (options.store) {
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}
复制代码

Store实现


我们在注入store之前,会先执行new Store来生成store实例。而Store是vuex中核心实现,我们来简单梳理下其实现。


我们执行实例化的时候肯定是先执行构造函数的

constructor (options = {}) {
  // 在非模块化开发中会自动执行安装,不需要执行Vue.install
  if (!Vue && typeof window !== 'undefined' && window.Vue) {
    install(window.Vue)
  }
  // 实例化store的限定条件可以了解下
  if (__DEV__) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
    assert(this instanceof Store, `store must be called with the new operator.`)
  }
  const {
    plugins = [],
    strict = false
  } = options
  // 一大堆相关变量
  // store internal state
  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  // 核心实现1 将options下的module进行递归处理,生成moduleTree,其中store配置被定义为root
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  this._watcherVM = new Vue()
  this._makeLocalGettersCache = Object.create(null)
  // 使用call函数绑定action和mutation的this为store
  // bind commit and dispatch to self
  const store = this
  const { dispatch, commit } = this
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
  }
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
  }
  // strict mode
  this.strict = strict
  // state对于到options下的第一层state
  const state = this._modules.root.state
  // 核心实现2 进行模块的注册安装
  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)
  // 核心实现3 创建vue实例来代理state
  // initialize the store vm, which is responsible for the reactivity
  // (also registers _wrappedGetters as computed properties)
  resetStoreVM(this, state)
  // vuex插件初始化
  // apply plugins
  plugins.forEach(plugin => plugin(this))
  const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
  if (useDevtools) {
    devtoolPlugin(this)
  }
}
复制代码


构造函数的执行实际覆盖了vuex中的大部分核心逻辑,我们上面列出了三处核心实现,下面我们对其进行进一步分析。


ModuleCollection实现


this._modules = new ModuleCollection(options)
复制代码


ModuleCollection实现在于将options进行递归处理,将module根据键名注册为Module树,其中顶层为{root: Module}

constructor (rawRootModule) {
  // register root module (Vuex.Store options)
  this.register([], rawRootModule, false)
}
复制代码
register (path, rawModule, runtime = true) {
  if (__DEV__) {
    assertRawModule(path, rawModule)
  }
  // 将module配置传入生成Module实例
  const newModule = new Module(rawModule, runtime)
  if (path.length === 0) {
    // 将root定义为根module
    this.root = newModule
  } else {
    // 如果不是根module则会通过path来寻找父module
    // 并且将其添加为module
    const parent = this.get(path.slice(0, -1))
    parent.addChild(path[path.length - 1], newModule)
  }
  // 遍历子模块进行module注册
  // register nested modules
  if (rawModule.modules) {
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}
复制代码


Module的实现比较简单,就是描述了模块及添加一些实例方法用于辅助父子模块的处理。这边就不展开进行分析了。


我们来看看ModuleCollection的输入输出

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count += 1
    }
  },
  getters: {
    getCount: (state) => {
      return state.count
    }
  },
  modules: {
    moduleA: {
      namespaced: true,
      state: {
        age: 1
      },
      getters: {
        getAge: (state) => {
          return state.age
        }
      },
      mutations: {
        incrementAge (state) {
          state.age += 1
        }
      }
    }
  }
})
复制代码


输出

{
  root: {
    runtime: false, 
    _children: {
      moduleA: {
        runtime: false, 
        _children: { }, 
        _rawModule: {
          namespaced: true, 
          state: {
            age: 1
          }, 
          getters: { }, 
          mutations: { }
        }, 
        state: {
          age: 1
        }
      }
    }, 
    _rawModule: {
      state: {
        count: 0
      }, 
      mutations: { }, 
      getters: { }, 
      modules: {
        moduleA: {
          namespaced: true, 
          state: {
            age: 1
          }, 
          getters: { }, 
          mutations: { }
        }
      }
    }, 
    state: {
      count: 0
    }
  }
}
复制代码


通过输入输出的对比可以发现,ModuleCollection将配置处理为module实例,主要暴露了state数据,并将其它数据放入_rawModule中,而子模块嵌套在_children对象中。


installModule实现

installModule的处理会复杂一些,我们同样分析代码再给出实例。


const state = this._modules.root.state
installModule(store, state, [], store._modules.root, true)
复制代码
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 通过getNamespace取得当前路径命名空间
  // 例如['moduleA', 'moduleAa'] => moduleA/moduleAa
  const namespace = store._modules.getNamespace(path)
  // 防重限制
  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }
  // set state
  if (!isRoot && !hot) {
    // getNestedState实际将取到rootSatte
    // 所以对于state来说
    // 会将模块的state通通添加为顶层state的属性
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // _withCommit将允许修改state设置为true
    // 此时可以修改state而不触发警告
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      // 使用setAPI动态添加属性
      // 为什么需要set?
      // 因为在动态注册新模块的时候也会执行installModule
      // 此时state在初始化(后面的步骤)中已经设置为响应式数据
      // 所以再添加需要使用setAPI
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // makeLocalContext是利用namespace取得各个模块下得局部数据
  const local = module.context = makeLocalContext(store, namespace, path)
  // 对于mutation来说
  // registerMutation将type=namespace+key作为键保存在store._mutations[type]中
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // 对于actions的逻辑和mutation差不多
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  // registerGetter则主要将getters注册到_wrappedGetters中
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // 递归子模块进行installModule
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
复制代码


对于installModule的分析已经基本完成,我们梳理下主要的几点


  1. 将模块state添加到顶层state下,键名为模块名称

  2. 使用makeLocalContext获取模块局部数据,将其传递给registerMutation进行方法注册等

  3. 递归子模块进行注册


在installModule处理之后,我们的store更新如下,重点的几个数据已经圈出


14.png


resetStoreVM实现


我们再来看看最后一个关键逻辑resetStoreVM的实现吧


我们前面的处理实际已经将options进行了初始化,并且根据命名空间进行一系列的处理。现在差的实际是响应式数据的实现。

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 将上步骤创建的store._wrappedGetters进行遍历
  forEachValue(wrappedGetters, (fn, key) => {
    // 将getters作为computed函数
    computed[key] = partial(fn, store)
    // 同时用getters代理store._vm
    // 这时我们再访问this.store.getters.xxx就会访问到store._vm[xxx]
    // 根据vue实例特性可知会访问到其中的computed函数
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 关键步骤将rootState作为data进行vue实例化
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent
  // enable strict mode for new vm
  // 对vm._data.$$state创建watcher实例进行监听修改
  // 如果不在commit中修改提示警告
  if (store.strict) {
    enableStrictMode(store)
  }
  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
复制代码


后话


记得很早之前看过vuex的实现原理,但是后面忘光了,再后来心态比较躺平就很畏惧源码了。其实vuex的原理并不复杂,主要是通过vue实例来创建响应式state。而其比较复杂的逻辑在于modules的处理,主要是执行了一系列的递归生成或处理树结构。


相关文章
|
3月前
|
资源调度 JavaScript
Vuex适合哪些场景使用
【8月更文挑战第5天】Vuex适合哪些场景使用
56 3
|
5月前
|
存储 JavaScript
|
6月前
|
存储 JavaScript 前端开发
vuex使用
vuex使用
|
6月前
|
存储 JavaScript API
vuex的使用
vuex的使用
31 0
|
存储 资源调度 JavaScript
Vuex详解,详细讲解一下Vuex
Vuex详解,详细讲解一下Vuex
215 0
|
6月前
|
存储 JavaScript
什么是vuex
什么是vuex
51 0
|
6月前
|
存储 JavaScript 安全
vuex总结
vuex总结
64 0
|
11月前
vuex
vuex
50 0
|
存储 JavaScript
关于Vuex的简单实际应用
关于Vuex的简单实际应用
关于Vuex的简单实际应用