Vuex 浅析

发布于 2023-10-26 14:10:31 字数 16982 浏览 36 评论 0

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

在 Vue 项目中引入 Vuex,需要采用插件引入方式。
Vue.use(Vuex)

安装 Vuex 插件,会调用插件中提供的 install 方法

// src/stiore.js
// store.js 的顶部定义了一个 Vue 变量,防止注册多次

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
  applyMixin(Vue)
  //这里调用 了 applyMixin 看名字应该是个混入,混入啥呢???
}

applyMixin 方法的主要功能将初始化 Vue 实例时传入的 store 设置到 this 对象的 $store 属性上, 子组件则从其父组件引用$store 属性, 层层嵌套进行设置. 这样, 任何一个组件都能通过 this.$store 的方式访问 store 对象了.

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
  //// 2.x 通过 hook 的方式注入
  // Vue 全局混入一个混入对象,在之后每个创建的 Vue 实例的 beforeCreate 生命周期中添加 vuexInit 函数
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // 使用自定义的 _init 方法并替换 Vue 对象原型的_init 方法,实现注入
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  
  function vuexInit () {
    const options = this.$options
    // 创建 Vue 根实例的 option
    // store injection store 注入
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 子组件从其父组件引用 $store 属性
      this.$store = options.parent.$store
    }
      }
    }

在 Vue 项目中,new 一个 Store 实例,并在创建 Vue 根实例时传入 store 实例


const store = new Vuex.Store({
  state,
  getters,
  mutations,
  actions,
  modules: {
      ...
  },
});

new Vue({
    store
    ...
})

接下来 Store 的构造函数

1、vuex 先对构造 store 需要的一些环境变量进行断言:

 
if (!Vue && typeof window !== 'undefined' && window.Vue) {
  // Vue 是全局变量时,自动 install
  install(window.Vue)
}

if (process.env.NODE_ENV !== 'production') {
 //  在非生产环境下,进行一些断言 
 //  当不满足参数 1 为 false 时抛出错误
  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.`)
}

2、初始化变量

// store internal state
// 是否在进行提交状态标识
this._committing = false
this._actions = Object.create(null)
// 监听所有的 action
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
// 监听所有的 mutation
this._subscribers = []
//创建一个 Vue 实例, 利用 $watch 监测 store 数据的变化
this._watcherVM = new Vue()
    

重点看下 this._modules = new ModuleCollection(options) 收集 modules 会调用 ModuleCollection,optinons 为 Store 构造函数传入的参数。从函数命名可以看出是模块收集注册的。

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    //注册根 module
    this.register([], rawRootModule, false)
  }

  ...

  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      // 检查模块内的 getters/ mutations/ actions 可迭代且是函数 或者 actions 的存在 handler 属性且属性值为函数
      assertRawModule(path, rawModule)
    }
    // 创建 module 对象
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
    // root 保存着根 module
      this.root = newModule
    } else {
     // 简单说 确定模块的父子关系
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
      // parent 为父 module,在 parent._children[path] = newModule
    }

    // register nested modules
    // 递归创建子 module
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
 ... 
}
forEachValue 是一个工具函数,对对象中的每个键值对调用函数
/**
 * forEach for object
 */
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

ModuleCollection 主要将传入的 options 对象整个构造为一个 module 对象, 并循环调用 register 为其中的 modules 属性进行模块注册, 使其都成为 module 对象, 最后 options 对象被构造成一个完整的组件树 。ModuleCollection new Module(rawModule, runtime) 来创建具体的 module。

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state
    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }
}

3、把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.

// 绑定 this 到 store
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)
}

dispatch 的功能是触发并传递一些参数(payload)给与 type 对应的 action
commit 的功能是触发并传递一些参数(payload)给与 type 对应的 mutation

4、store 其他重要属性的配置

// 确保 dispatch/commit 方法中的 this 对象正确指向 store

this.strict = strict

// 根 module 的 state

const state = this._modules.root.state

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.forEach(plugin => plugin(this))

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)
}




// installModule 方法则会将处理过的 modules 进行注册和安装,
/// installModule 接收 5 个参数: store、rootState、path、module、hot.
// store 表示当前 Store 实例, rootState 表示根 state, path 表示当前嵌套模块的路径数组
// module 表示当前安装的模块,
// hot 当动态改变 modules 或者热更新的时候为 true
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 根据 path 或者路径上各层模块的 namespace
  const namespace = store._modules.getNamespace(path)
  // 根据 path 数组 寻找路径上的模块,把各模块的 namespaced 拼接起来 

  // register in namespace map
 // 在 store 上的 _modulesNamespaceMap 注册有 namesoaced 的模块
 // 方便后面根据 namespace 属性 直接获取模块
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  // 当 !isRoot 为 true 说明 path.length > 0
  // 当 !hot 为 true,说明 hot 为 false
  if (!isRoot && !hot) {
    // 根据 path 找到父级模块
    // 可知,在父 module 的 state 中通过 path 路径名注册子 state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 给模块绑定上下文在 mapActions 等辅助函数时有用
  // 该方法其实是在重写 dispatch 和 commit 函数
  // 你是否有疑问模块中的 dispatch 和 commit
  // 是如何找到对应模块中的函数的
  // 假如模块 A 中有一个名为 add 的 mutation
  // 通过 makeLocalContext 函数,会将 add 变成
  // a/add,这样就可以找到模块 A 中对应函数了

  const local = module.context = makeLocalContext(store, namespace, path)
 
  //给模块上下文绑定处理过 type 的 dispatch,mutation,getters,
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    //向 store._mutations[type] 数组添加处理函数
    // local 传递,保证 mutation 的第一个参数是模块内部的 state === local.state
    registerMutation(store, namespacedType, mutation, local) // store._mutations[type] type 为 namespacedType
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    // local 传递 local.dispatch,local.commit,local.getters,local.state
    // type 作用只为找到 store._actions[type] 数组 添加处理函数
    registerAction(store, type, handler, local) 
    // store._actions[type] type 为 namespacedType
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local) 
    // 在 store._wrappedGetters[type] (type 为 namespacedType)
    // 向 module 内部的 getter 传递 local.state,local.getters, root state, root getters,
  })
  // 循环注册子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

简单提下如何模块的 context 是怎么处理的吧

function makeLocalContext (store, namespace, path) {
  // 是否存在命名空间
  const noNamespace = namespace === ''

  const local = {
   // 根 module 与子 module 都是调用 store.dispatch
   // 子 module 的 type 会经过命名空间处理
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        // 非生产环境且 actions[type] 不存在
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },
    // 根 module 与子 module 都是调用 store.commit
   // 子 module 的 type 会经过命名空间处理
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace) //返回一个新的对象,属性是 localType,value 是 store.getters[namespacedType]
    },
    state: {
      get: () => getNestedState(store.state, path) 
      // 根据 path 数据返回模块内部的 state
      // 在根 state 上一层层取值获取
    }
  })

  return local
}

5、上面可以看到有个函数 resetStoreVM(this, state) ,原注释是初始化 store vm,利用 Vue 的数据响应系统来监听内部变化,同时让 store.__wrappedGetters 变成绑定成 vm 的计算属性,响应变化。

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  //定义 getters 属性
  store.getters = {}
  // 获取处理的 getters 函数集合
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 循环所有处理过的 getters,
  // 并新建 computed 对象进行存储 getter 函数执行的结果,
  // 然后通过 Object.defineProperty 方法为 getters 对象建立属性
  // 使得我们通过 this.$store.getters.xxxgetter 能够访问到 store._vm[xxxgetters]
  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 enviroment.
    computed[key] = partial(fn, store)
   // 等价于 computed[key] = () => fn(store)
    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 // 取消 Vue 的所有日志与警告
  // 设置新的 vm, 传入 state
  // 把 computed 对象作为 _vm 的 computed 属性, 这样就完成了 getters 的注册
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent // 恢复用户配置

  // enable strict mode for new vm
  if (store.strict) {
    // 严格模式下, 在 mutation 之外的地方修改 state 会报错
    enableStrictMode(store)
  }
  
 // 销毁旧的 vm 实例
  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())
  }
}

上文提到严格模式,是如何控制严格模式的呢

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
  // 利用 store 上的 _vm 属性指向的 vue 实例的 watch 时刻观察 store.state 变化。 
  // 回调函数为断言 store._committing 是否为 true,为 true 说明是 mutation
}

6、了解 vuex 的文档,可知 vuex 提供订阅 mutation 和 action 的功能,这又是如何实现的?可以自主想想,是不是可以在每次调用 mutiaon,action 时 回调你的 handler 呢?来看下 commit 和 dispathc 里面的处理吧

在 store 的属性配置里初始化了
 // 监听所有的 action
this._actionSubscribers = []

// 监听所有的 mutation
    this._subscribers = []


// 订阅 mutation
subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
}
//订阅 store 的 action。
  // handler 会在每个 action 分发的时候调用并接收
  //  action 描述和当前的 store 的 state 这两个参数:
subscribeAction (fn) {
// 默认是 before
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
}

// 没有就添加 有就删除
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

// 目前可知 mutation 订阅时往 this._subscribers 数组中 push 处理函数,action 订阅时往 this._actionSubscribers 数组中 push 处理函数

// 接下来简单看下 commit 时处理触发订阅呢
 commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 是这里呢
     this._subscribers.forEach(sub => sub(mutation, this.state))
    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }
  
  
   dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // 调用 before 时机的处理函数
    // 在 before 和 after 不存在的情况默认是 before
    try {
      this._actionSubscribers
        .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)
      }
    }

    const result = entry.length > 1
    // handler 返回的肯定是一个 Promise
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
    // 调用 after 时机的处理函数
    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
    })
  }

其实还有很多没提,比如 mapXXX 等辅助函数是如何实现,如果动态添加插件,这些大家有兴趣可以去探索呢

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

呢古

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

13886483628

文章 0 评论 0

流年已逝

文章 0 评论 0

℡寂寞咖啡

文章 0 评论 0

笑看君怀她人

文章 0 评论 0

wkeithbarry

文章 0 评论 0

素手挽清风

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文