Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuex 浅析 #10

Open
Lindysen opened this issue May 29, 2019 · 0 comments
Open

Vuex 浅析 #10

Lindysen opened this issue May 29, 2019 · 0 comments

Comments

@Lindysen
Copy link
Owner

Lindysen commented May 29, 2019

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 对象被构造成一个完整的组件树 。ModuleCollectionnew 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
    }
  }
}

  1. 把 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
  1. 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
}
  1. 上面可以看到有个函数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
}
  1. 了解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 等辅助函数是如何实现,如果动态添加插件,这些大家有兴趣可以去探索呢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant