资讯专栏INFORMATION COLUMN

深入学习Vuex

funnyZhang / 2495人阅读

摘要:深入学习作为配合使用的数据状态管理库,针对解决兄弟组件或多层级组件共享数据状态的痛点问题来说,非常好用。至此,构造函数部分已经过了一遍了。

深入学习Vuex

vuex作为配合vue使用的数据状态管理库,针对解决兄弟组件或多层级组件共享数据状态的痛点问题来说,非常好用。本文以使用者的角度,结合源码来学习vuex。其中也参考了许多前辈的文章,参见最后的Reference

Vue加载Vuex(Vue.use(Vuex))

Vue加载Vuex还是很简单的,让我们以官方文档上实例为切入点来开始认识Vuex

import Vue from "vue"
import Vuex from "vuex"
import cart from "./modules/cart"
import products from "./modules/products"
import createLogger from "../../../src/plugins/logger"

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== "production"

export default new Vuex.Store({
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

这段代码我们再熟悉不过了,就是Vue加载Vuex插件,然后new了一个Vuex实例。
我们一步一步来看,首先看一下Vue如何加载的Vuex,也就是Vue.use(Vuex)发生了什么。

Vue.use = function (plugin: Function | Object) {
    /* istanbul ignore if */
    /*标识位检测该插件是否已经被安装*/
    if (plugin.installed) {
      return
    }
    // additional parameters
    const args = toArray(arguments, 1)
    /*将this(Vue构造函数)加入数组头部*/
    args.unshift(this)
    if (typeof plugin.install === "function") {
      /*install执行插件安装*/
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === "function") {
      plugin.apply(null, args)
    }
    //标记插件已安装
    plugin.installed = true
    return this
  }

主要做了几件事:

验证是否已安装,避免重复

如果插件提供install方法,则执行否则把插件当作function执行

最后标记插件已安装

那么Vuex提供install方法了吗?答案是肯定的

let Vue // bind on install
export function install (_Vue) {
  if (Vue) {
    /*避免重复安装(Vue.use内部也会检测一次是否重复安装同一个插件)*/
    if (process.env.NODE_ENV !== "production") {
      console.error(
        "[vuex] already installed. Vue.use(Vuex) should be called only once."
      )
    }
    return
  }
  /*保存Vue,同时用于检测是否重复安装*/
  Vue = _Vue//Vue构造函数
  /*将vuexInit混淆进Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
  applyMixin(Vue)
}

看mixin之前我们可以先思考一个问题,我们在访问Vuex的数据的时候基本都是这样访问的,比如this.user = this.$store.state.global.user,this.$store是什么时候加到Vue实例上的?applyMixin会给出答案,让我们继续看applyMixin发生了什么

// applyMixin:
export default function (Vue) {
  /*获取Vue版本,鉴别Vue1.0还是Vue2.0*/
  const version = Number(Vue.version.split(".")[0])

  if (version >= 2) {
    /*通过mixin将vuexInit混淆到Vue实例的beforeCreate钩子中*/
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    /*将vuexInit放入_init中调用*/
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */
   /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/
  function vuexInit () {
    // this  = vue object
    const options = this.$options
    // store injection
    if (options.store) {
      /*存在store其实代表的就是Root节点,直接执行store(function时)或者使用store(非function)*/
      this.$store = typeof options.store === "function"
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /*子组件直接从父组件中获取$store,这样就保证了所有组件都公用了全局的同一份store*/
      this.$store = options.parent.$store
    }
  }
}

我们这里就只看2.0了,思路就是通过Vue.mixin把挂载$store的动作放在beforeCreate钩子上,由此实现了每个组件实例都可以通过this.$store来直接访问数据。
注意:mixin的细节

同名钩子函数将混合为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。

至此,Vue.use(Vuex)我们已经了解完了。

Vuex结构总览

顺着我们实例代码的思路,接下来我们应该开始看构造器了,不过开始看之前,我们先看一下Vuex.store Class都定义了些什么。

export class Store {
  constructor (options = {}) {
  }
    
  // state 取值函数(getter)
  get state () {
  }
  //存值函数(setter)
  set state (v) {
  }

  /* 调用mutation的commit方法 */
  commit (_type, _payload, _options) {
  }

  /* 调用action的dispatch方法 */
  dispatch (_type, _payload) {
  }

  /* 注册一个订阅函数,返回取消订阅的函数 */
  subscribe (fn) {
  }

  /* 观察一个getter方法 */
  watch (getter, cb, options) {
  }

  /* 重置state */
  replaceState (state) {
  }

  /* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */
  registerModule (path, rawModule) {
  }

  /* 注销一个动态module */
  unregisterModule (path) {
  }

  /* 热更新 */
  hotUpdate (newOptions) {
  }

  /* 保证通过mutation修改store的数据 */
  // 内部使用,比如当外部强行改变state的数据时直接报错
  _withCommit (fn) {
  }
}

以上就是定义的接口了,官方文档上实例属性和方法都在这里找得到。
来看一张大神画的图有助于理解思路

出处见最后。
接下来我们继续实例思路

export default new Vuex.Store({
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})

来看一下构造函数

constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    /*
      在浏览器环境下,如果插件还未安装(!Vue即判断是否未安装),则它会自动安装。
      它允许用户在某些情况下避免自动安装。
    */
    if (!Vue && typeof window !== "undefined" && window.Vue) {
      install(window.Vue) // 将store注册到实例或conponent
    }

    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.`)
      //检查是不是new 操作符调用的
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      /*一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)*/
      plugins = [],
      /*使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。*/
      strict = false
    } = options

    /*从option中取出state,如果state是function则执行,最终得到一个对象*/
    let {
      state = {}
    } = options
    if (typeof state === "function") {
      state = state()
    }

    // store internal state
    /* 用来判断严格模式下是否是用mutation修改state的 */
    this._committing = false
    /* 存放action */
    this._actions = Object.create(null)
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    //包装后的getter
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根据namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放订阅者 外部插件使用 */
    this._subscribers = []
    /* 用以实现Watch的Vue实例 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
    const store = this
    const {dispatch, commit} = this
    /* 为dispatch与commit绑定this(Store实例本身) */
    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
    /*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    /*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
    resetStoreVM(this, state)

    // apply plugins
    /* 调用插件 */
    plugins.forEach(plugin => plugin(this))

    /* devtool插件 */
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

Vuex的源码一共就一千行左右,构造函数吃透基本掌握至少一半了,构造函数中主要是初始化各种属性。简单的详见注释,这里我们主要看如何解析处理modules,首先来看this._modules = new ModuleCollection(options),ModuleCollection结构如下

import Module from "./module"
import { assert, forEachValue } from "../util"

/*module收集类*/
export default class ModuleCollection {
  constructor (rawRootModule) { // new store(options)
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  /*获取父级module*/
  get (path) {
  }

  /*
    获取namespace,当namespaced为true的时候会返回"moduleName/name"
    默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
    如果希望你的模块更加自包含或提高可重用性,你可以通过添加 namespaced: true 的方式使其成为命名空间模块。
    当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
  */
  getNamespace (path) {
  }

  update (rawRootModule) {
  }

  /*注册*/
  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) {
      /*path为空数组的代表跟节点*/
      this.root = newModule
    } else {
      /*获取父级module*/
      const parent = this.get(path.slice(0, -1))//排除倒数第一个元素的数组,
      /*在父module中插入一个子module*/
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    /*递归注册module*/
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // concat不改变源数组,返回合并后的数组
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  /*注销*/
  unregister (path) {
  }
}
/*Module构造类*/
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    /*保存module*/
    this._rawModule = rawModule
    /*保存modele的state*/
    const rawState = rawModule.state
    this.state = (typeof rawState === "function" ? rawState() : rawState) || {}
  }

  /* 获取namespace */
  get namespaced () {
  }

  /*插入一个子module,存入_children中*/
  addChild (key, module) {
    this._children[key] = module
  }

  /*移除一个子module*/
  removeChild (key) {
  }

  /*根据key获取子module*/
  getChild (key) {
    return this._children[key]
  }

  /* 更新module */
  update (rawModule) {
  }
  /* 遍历child  */
  forEachChild (fn) {
  }

  /* 遍历getter */
  forEachGetter (fn) {
  }

  /* 遍历action */
  forEachAction (fn) {
  }

  /* 遍历matation */
  forEachMutation (fn) {
  }
}

ModuleCollection的作用就是收集和管理Module,构造函数中对我们传入的options的module进行了注册(register 是重点,详见注释,注册方法中使用了递归,以此来注册所有的module)。我们给出的实例经过ModuleCollection的收集之后变成了什么样子呢?

//this._modules简单结构示例
{
  root: {
    state: {},
    _children: {
      cart: {
        ...
      },
      products: {
        ...
      }
    }
  }
}

收集完了之后,我们看一下是如何初始化这些module的installModule(this, state, [], this._modules.root)

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

  // register in namespace map
  /* 如果有namespace则在_modulesNamespaceMap中注册 */
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    /* 获取父级的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))//深度取值,并返回取到的值
    /* module的name */
    const moduleName = path[path.length - 1]
    store._withCommit(() => {// 添加响应式属性
      /* 将子module设置称响应式的 */
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 当前模块上下文信息
  const local = module.context = makeLocalContext(store, namespace, path)

  /* 遍历注册mutation */
  //mutation:key对应的handler
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  /* 遍历注册action */
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  /* 遍历注册getter */
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  /* 递归安装mudule */
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

在这里构造了各个module的信息也就是localConext,包括各个模块的mutation,action ,getter ,mudule ,其中运用到了许多包装的技巧(主要为了计算模块的路径),还有代理的方式访问数据,详见注释

resetStoreVM方法思路就是借助Vue响应式来实现Vuex的响应式

/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放之前的vm对象 */
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
  // key = wrappedGetters的key,fn = wrappedGetters的key对应的value
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    // store.getter并没有像state一样在class直接注册了getter,setter,而是在这里定义的
    // 用过代理的方式借助Vue计算属性实现了Vuex的计算属性
    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的目的是在new一个Vue实例的过程中不会报出一切警告 */
  Vue.config.silent = true
  /*  这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  //通过Vue的数据劫持,创造了dep,在Vue实例中使用的话Watcher会收集依赖,以达到响应式的目的
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使能严格模式,保证修改store只能通过mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
    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也构造为响应式的,store._vm指向一个Vue的实例,借助Vue数据劫持,创造了dep,在组件实例中使用的话Watcher会收集依赖,以达到响应式的目的。
至此,构造函数部分已经过了一遍了。

最后

本文主要是学习了Vuex的初始化部分,实际的Vuex的api接口的实现也有相关的中文注释,我已经把主要部分中文注释代码放在这里,需者自取中文注释代码
学习过程中参考了许多大神的文章,一并感谢。

Reference

Vue.js 源码解析(参考了大神的许多注释,大神写的太详尽了,我只补充了一部分)
Vuex框架原理与源码分析

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/52589.html

相关文章

  • 一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构

    摘要:前言本文内容讲解的内容一张思维导图辅助你深入了解源码架构。总结以上内容是笔者最近学习源码时的收获与所做的笔记,本文内容大多是开源项目技术揭秘的内容,只不过是以思维导图的形式来展现,内容有省略,还加入了笔者的一点理解。1.前言 本文内容讲解的内容:一张思维导图辅助你深入了解 Vue | Vue-Router | Vuex 源码架构。 项目地址:github.com/biaochenxuy… 文...

    weij 评论0 收藏0
  • 深入学习Vuex

    摘要:深入学习作为配合使用的数据状态管理库,针对解决兄弟组件或多层级组件共享数据状态的痛点问题来说,非常好用。至此,构造函数部分已经过了一遍了。 深入学习Vuex vuex作为配合vue使用的数据状态管理库,针对解决兄弟组件或多层级组件共享数据状态的痛点问题来说,非常好用。本文以使用者的角度,结合源码来学习vuex。其中也参考了许多前辈的文章,参见最后的Reference Vue加载Vuex...

    codercao 评论0 收藏0
  • 深入理解js

    摘要:详解十大常用设计模式力荐深度好文深入理解大设计模式收集各种疑难杂症的问题集锦关于,工作和学习过程中遇到过许多问题,也解答过许多别人的问题。介绍了的内存管理。 延迟加载 (Lazyload) 三种实现方式 延迟加载也称为惰性加载,即在长网页中延迟加载图像。用户滚动到它们之前,视口外的图像不会加载。本文详细介绍了三种延迟加载的实现方式。 详解 Javascript十大常用设计模式 力荐~ ...

    caikeal 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<