Vuex源码解析

3 篇文章 1 订阅
1 篇文章 0 订阅

Vuex 源码解析

前言

本渣最近心血来潮学习了一下 Vuex 的源码,再次做一下整理和分享。
Vuex 作为一个专为 Vue.js 程序开发的状态管理模式,想必大家都很熟悉。那么大家在开发的过程中有没有想过遗下几个问题:

  • Vuex 是怎么挂载到 Vue 上的呢
  • Vuex 的数据响应式是如何实现的
  • Vuex 的严格模式又是怎么回事
  • 为什么 state 能够在所有子组件中使用 store

带着这些问题,我们一起看一下 Vuex 的源码:

这里首先上一张官网截图:

在这里插入图片描述

Vuex 挂载

vue 使用插件的方式只需要 Vue.use(plugin),对于 Vuex 就是Vue.use(Vuex).我们都知道 Vue 源码挂载插件部分,如果参数有 install 方法,则执行这个 install 方法。如果参数本身是一个 function,那么只会执行这个 function
那么我们下面看一下 Vuex 插件的 install 方法都做了一些什么事情:

store.js

export function install(_Vue) {
  //避免重复安装
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue //存储Vue实例
  //混入操作,主要是将vuexInit方法混入到Vue的beforeCreate钩子函数中
  applyMixin(Vue)
}

从上面的代码,我们可以看出,install 方法接收一个 Vue 实例,然后先判断是否已经安装过 Vuex,后面存储该 Vue 实例,最后执行了一下混入操作。
然后我们来看一下这个混入操作:

mixin.js

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])
  //根据Vue的版本号来控制混入,2.0以上的就是用混入
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init ? [vuexInit].concat(options.init) : vuexInit
      _init.call(this, options)
    }
  }
  //init初始化函数,主要是挂载全局变量$store
  function vuexInit() {
    const options = this.$options
    //如果当前节点为跟节点,则直接从options选项中获取store
    if (options.store) {
      this.$store =
        typeof options.store === 'function' ? options.store() : options.store
      //如果不是根节点,则通过options中的parent获取父组件的$store引用
    } else if (options.parent && options.parent.$store) {
      //子组件获取父组件的$store,每个子组件都能够获得到store对象
      this.$store = options.parent.$store
    }
  }
}

该方法首先检测了一下 Vue 的版本,如果是 2.0 以上,则使用 Vue.mixin()方法,将初始化函数混入 Vue 的 beforeCreate 钩子函数中。如果 Vue2.0 以下的版本,则将初始化函数挂载到 Vue 原型的_init 属性上。
vueInit 函数主要是将 Store 实例挂载到 Vue 实例的$store 属性上。这里对是否是跟节点进行了判断,如果是根节点的话直接取值’options.store’,如果是组件(组件存在 options.parent 属性)的话,则取 options.parent.$store 即父组件的 Store。这样就保证了所有的子组件都能够取到相同的 Store。这样就保证了所有的子组件都能够取到相同的。
同时,这也解决了之前我们提到的问题:为什么 state 能够在所有子组件中使用 store。
我们都知道 options.store 即是Store的实例。所以,我们下面开始看看 Store 的构造函数到底做了什么事情:

Store

我们先看一下 store.js 中的 Store 的构造函数:

该构造函数首先判断是否已经安装了 Vuex,如果未安装则重新安装 Vuex 插件。然后检测当前环境。

//在浏览器环境下,如果插件还未安装,则会自动安装
if (!Vue && typeof window !== 'undefined' && window.Vue) {
  //也就是说当Vue不生效时使用window下的Vue,他啥时候放上去的
  install(window.Vue)
}

//判断在非生产环境:必须调用Vue进行注册,必须支持Promise,必须用过new 创建Store
if (process.env.NODE_ENV !== 'production') {
  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.`)
}

之后就是进行一系列的属性初始化操作,其中包含了是否开启严格模式,初始化 actions、mutations、getters 等属性。
这其中的重中之重就是 ModuleCollection,该属性的主要作用就是构造 module 对象,后面会详细分析

const {
  plugins = [],
  //是否开启Vuex的严格模式,在严格模式下,任何mutations函数以外的方式修改state都会抛出错误
  strict = false,
} = options

// 用来判断严格模式下是否是用mutation修改state
this._committing = false
//actions,存放actions
this._actions = Object.create(null)
//action订阅函数
this._actionSubscribers = []
//mutations
this._mutations = Object.create(null)
//存放getter
this._wrappedGetters = Object.create(null)
//构建module对象
this._modules = new ModuleCollection(options)
//根据namespace存放module
this._modulesNamespaceMap = Object.create(null)
//存放订阅者
this._subscribers = []
//存储Vue实例,主要是使用其中的watch
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)

初始化属性之后,接下来就是处理 commit 和 dispatch 方法了,这一步的主要目的是改变两个方法中的 this 指向,将其指向当前的 store。

const { dispatch, commit } = this
//下面两个方法都是改变dispatch/commit两个方法的this指向
this.dispatch = function boundDispatch(type, payload) {
  return dispatch.call(store, type, payload)
}
//type:方法名,payload:载荷 options:配置项
this.commit = function boundCommit(type, payload, options) {
  return commit.call(store, type, payload, options)
}

下面主要是模块化的处理、数据响应式的处理、插件注册等一系列的操作。下面我们来仔细看一下:

// 是否开启严格模式
this.strict = strict

//获取根模块state
const state = this._modules.root.state

//只有根模块才有root属性
//模块化处理,递归的处理所有的子模块
installModule(this, state, [], this._modules.root)

//通过使用vue实例,初始化store._vm,使state变成可响应的,并且将getters变成计算属性(代理属性)
resetStoreVM(this, state)

//注册插件
plugins.forEach((plugin) => plugin(this))
//调试工具注册
const useDevtools =
  options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)
}

至此,就是 store 构造函数全部内容了,其主要操作就是初始化各种属性,模块化的处理,数据响应式的处理等一些操作。
下面我们先来看一下模块化的实现

模块化

什么是模块化,这里引用官网上的一段话:

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

由 store 构造函数我们可知,module 的初始化是由 ModuleCollection 完成的,那么我们接下来看一下 ModuleCollection 都做了些什么事情

module-collection.js

其构造函数主要是注册根模块

  //注册根module
  register(path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    //调用Module的构造函数
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      //将module挂载根root上
      this.root = newModule
    } else {
      //注册子module,将子module添加到父module的_children属性上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }
    //注册嵌套模块,如果有子modules,循环注册
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

关于注册函数的主要内容就是构造 Module 实例化对象,然后递归处理子模块,以确保我们可以通过 store.state.rawChildModule 获取子模块的 state 状态。
接下来我们看一下 Module 的内容
module.js

export default class Module {
  constructor(rawModule, runtime) {
    //初始值为false
    this.runtime = runtime
    //存储子模块
    this._children = Object.create(null)
    //将原来的module存储,以备后续使用
    this._rawModule = rawModule
    const rawState = rawModule.state

    //存储原来module的state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  addChild() {}
  removeChild() {}
  getChild() {}
  update() {}
  //...
}

Module 的内容其实很简单,主要就是提供了个中操作 module 的各种方法。
至此,我们理清了 ModuleCollection 的大致功能,就是接收我们创建 Store 传入的 options 选项,然后使用 options 构造一个 Module 对象,同时递归处理。最终构建一个 Module 树。

Module 树构建完成以后,我们继续看 Store 的构造函数。这个时候通过installModule方法处理该 Module 树,installModule 的函数比较长,这里只粘贴部分代码:

function installModule(store, rootState, path, module, hot) {
  //是否是根module,根节点的path=[]
  const isRoot = !path.length
  //获取module的namespace
  const namespace = store._modules.getNamespace(path)

  //如果有namespace,则在_moduleNamespaceMap中注册
  if (module.namespaced) {
    if (
      store._modulesNamespaceMap[namespace] &&
      process.env.NODE_ENV !== 'production'
    ) {
      console.error(
        `[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
          '/'
        )}`
      )
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  //不是根节点,把子组件的每一个state设置到其父级的state属性上
  if (!isRoot && !hot) {
    //获取父级的state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    //获取当前module的名字
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (process.env.NODE_ENV !== 'production') {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
              '.'
            )}"`
          )
        }
      }
      //通过Vue.set()方法将当前的module的state挂载到父state上,通过Vue将子module设置成响应式
      Vue.set(parentState, moduleName, module.state)
    })
  }
  //给context对象赋值,设置局部的dispatch、commit方法以及getters和state
  const local = (module.context = makeLocalContext(store, namespace, path))
  //遍历注册mutation
  module.forEachMutation()
  //遍历注册action
  module.forEachAction()
  //遍历注册getter
  module.forEachGetter()
  //递归处理module
  module.forEachChild()
}

以上便是 installModule 方法的大致内容了。我们可以看到,installModule 函数先判断了当前节点是否是根节点以及是否设置了命名空间 namespace。如果是根节点则将 module 挂载到 store._modulesNamespaceMap 上,否则将子组件的 state 挂载到到父级的 state 属性上。
这步主要是通过 Vue.set 来完成。然后通过 makeLocalContext 方法将 dispatch、commit、getters 和 state 挂载到 module 的 context 属性上,这个过程也是递归处理的。

然后我们退回之前的 Store 构造函数的后续操作resetStoreVM()

响应式

我们都知道 Vuex 中的数据都是响应式的,而 resetStoreVM()的主要功能就是实现数据的响应化。下面我们来看一下 resetStoreVM 的内容
store.js

//存放旧的Vue实例对象,该实例对象的主要作用就是实现state和computed的响应化
const oldVm = store._vm

//初始化Store中的getters
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
//通过Object.defineProperty的数据拦截功能,为每一个getter设置get方法,其实就是数据代理
forEachValue(wrappedGetters, (fn, key) => {
  // use computed to leverage its lazy-caching mechanism
  // direct inline function use will lead to closure preserving oldVm.
  // using partial to return function with only arguments preserved in closure environment.
  computed[key] = partial(fn, store)
  //在getters上添加属性
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true, // for local getters
  })
})

由上面的代码我们可以看出,restoreVM 首先初始化 store 实例的 getters,然后利用 Object.defineProperty 的数据拦截功能实现数据代理,这样我们就可以通过 store.getters 访问 getters 里面的内容了。

除了数据代理之外,我们来看一下后面的内容:

const silent = Vue.config.silent
//暂时设置为true的目的是在new一个Vue实例的过程中不会报出警告
Vue.config.silent = true
//new一个Vue实例对象,运用Vue内部的响应式实现注册state以及computed,所以Vuex和Vue是强绑定的
store._vm = new Vue({
  data: {
    $$state: state,
  },
  computed,
})
Vue.config.silent = silent

//使用严格模式,保证修改store只能通过mutation
if (store.strict) {
  enableStrictMode(store)
}

//如果旧的vm存在
if (oldVm) {
  //解除旧的vm的state饮用,以及销毁旧的Vue对象
  if (hot) {
    store._withCommit(() => {
      oldVm._data.$$state = null
    })
  }
  Vue.nextTick(() => oldVm.$destroy())
}

从上面的代码中我们可以看出,后续操作主要是通过 Vue 实现 state 和 computed 的响应化,然后接着判断是会否开启严格模式。最后,如果存在旧的 Vue 实例,我们还要通过 Vue.nextTick()方法销毁旧的 Vue 实例。

总结一下 resetStoreVM()方法,主要是实现了 getter 的代理、state 和 computed 的数据响应化。同时,他也判断了是否开启严格模式。下面我们来看一下严格模式的具体实现吧:enableStrictMode(store).

严格模式

借用 Vuex 官网上的一段话:

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

那么我们接下来看看,他的严格模式的跟踪到底是怎么实现的:
store.js

function enableStrictMode(store) {
  store._vm.$watch(
    function () {
      return this._data.$$state
    },
    () => {
      if (process.env.NODE_ENV !== 'production') {
        //断言检测,检查store中的_committing的值,如果是true则代表不是通过mutation方法修改的
        assert(
          store._committing,
          `do not mutate vuex store state outside mutation handlers.`
        )
      }
    },
    { deep: true, sync: true }
  )
}

enableStrictMode()方法的内容很简单,其实就是接收一个 Store 实例,然后利用 Store 实例上挂载的 Vue 实现对 state 数据的监听。这里的监听主要是判断 store._committing,如果为 false 则抛出错误告诉用户需要使用 mutations 来修改数据。
store._committing 我们都知道是在 Store 的构造函数中初始化的,那么我们来看一下 store._committing 是在什么地方被标记的:

//store中的commit方法,执行mutation语句
_withCommit(fn) {
    //保存之前的提交状态
    const committing = this._committing
    //进行本次提交,如不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告
    this._committing = true
    fn()
    //修改完成后还原本次修改之前的状态
    this._committing = committing
  }
}

其实该方法就是 store 中的 commit 方法,方法很简单。他最终执行的是 传入的 mutation 语句。这里会将_committing 设置为了 true。

总结一下 Vuex 的严格模式,就是在 Store 初始化的时候设置了一个标识用来判断是否是 mutation 提交的数据。在严格模式下,通过Vue.watch来监听数据的变化,如果不是 mutations 提交的数据则抛出错误。

commit(mutation) 和 dispatch(action)

commit

众所周知,commit 的主要作用就是提交 mutation,那么我们来看一下 commit 都做了一些什么事情,这里我们只看部分比较重要的代码:

store.js

  //commit函数先进行适配处理,然后判断当前action type是否存在,如果存在则调用_withCommit 函数执行相应的mutation
  commit(_type, _payload, _options) {
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    //取出type对应的mutation方法
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    //执行mutation中的所有方法
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })
    //通知所有订阅者
    this._subscribers(...)
  }

可以看出 commit 的主要作用就是提调用_withCommit()方法提交 mutation。这里的_withCommit 就是我们之前提到的那个_withCommit 方法。

dispatch

dispatch 的作用是调用 action。他的原理和 commit 是相同的,唯一不同的地方就在于因为action(所以 action 能够执行异步)是一个 Promise,所以 dispatch 需要调用 Promise.all 方法来执行所有的 action 函数。
这里我们只粘贴 dispatch 相较于 commit 不同的代码:
store.js

try {
  this._actionSubscribers
    .slice()
    .filter((sub) => sub.before)
    .forEach((sub) => sub.before(action, this.state))
} catch (e) {
  if (process.env.NODE_ENV !== 'production') {
    console.warn(`[vuex] error in before action subscribers: `)
    console.error(e)
  }
}
//是数组则包装Promise形成一个新的Promise,只有一个则返回第0个
const result =
  entry.length > 1
    ? Promise.all(entry.map((handler) => handler(payload)))
    : entry[0](payload)

//执行最后的Promise
return result.then((res) => {
  try {
    this._actionSubscribers
      .filter((sub) => sub.after)
      .forEach((sub) => sub.after(action, this.state))
  } catch (e) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(`[vuex] error in after action subscribers: `)
      console.error(e)
    }
  }
  return res
})

工具函数

我们平常所用的一些 mapState、mapAction、mapGetters 等哦那工具函数均是在 helper.js 文件中实现的。
这些方法的实现都是大同小异的,简单来说都是接收传入的 namespace,然后调用 store._modulesNamespaceMap 映射,获取相应的 state、actions 等数据。

因为方法很简单,这里只是简单的介绍一下,就不粘贴代码了。

做一个自己的 Vuex

Vuex 的代码还是比较简单的,代码量也很少。我们完全可以结合 Vuex 官网,将其作为一个文档来看。通过对 Vuex 源码的解析,我们完全可以实现一个简单的 Vuex 框架,以便加深我们对 Vuex 框架的理解。
这里附上写的简单的 Vuex 框架的地址:

做一个自己的 Vuex

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值