vuex源码简析

Posted guolao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vuex源码简析相关的知识,希望对你有一定的参考价值。

前言


基于 vuex 3.12 按如下流程进行分析:

Vue.use(Vuex);

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

Vue.use(Vuex)


Vue.use() 会执行插件的 install 方法,并把插件放入缓存数组中。

而 Vuex 的 install 方法很简单,保证只执行一次,以及使用 applyMixin 初始化。

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 方法定义在 vuex/src/mixin.js,vuex 还兼容了 vue 1 的版本,这里只关注 vue 2 的处理。

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

  if (version >= 2) {
    // 混入一个 beforeCreate 方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 这里是 vue 1 的兼容处理
  }

  // 这里的逻辑就是将 options.store 保存在 vue 组件的 this.$store 中,
  // options.store 就是 new Vue({...}) 时传入的 store,
  // 也就是 new Vuex.Store({...}) 生成的实例。
  function vuexInit () {
    const options = this.$options
    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
    }
  }
}

new Vuex.Store({...})


先看 Store 的构造函数:

constructor (options = {}) {
  const { plugins = [], strict = false } = options

  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  // 构建 modules
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  // store.watch
  this._watcherVM = new Vue()
  this._makeLocalGettersCache = Object.create(null)

  // 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
    // 安装模块
  const state = this._modules.root.state
  installModule(this, state, [], this._modules.root)

  // 实现状态的响应式
  resetStoreVM(this, state)

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

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

this._modules

vuex 为了让结构清晰,允许我们将 store 分割成模块,每个模块拥有自己的 statemutationactiongetter,而且模块自身也可以拥有子模块。

const moduleA = {...}
const moduleB = {...}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

而模块在 vuex 的构造函数中通过new ModuleCollection(options)生成,依然只看构造函数:

// vuex/src/module/module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  register (path, rawModule, runtime = true) {
    // 生成一个 module
    const newModule = new Module(rawModule, runtime)
    // new Vuex.Store() 生成的是根模块
    if (path.length === 0) {
      // 根模块
      this.root = newModule
    } else {
      // 生成父子关系
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 注册嵌套的模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

register 接收 3 个参数,其中 path 表示路径,即模块树的路径,rawModule 表示传入的模块的配置,runtime 表示是否是一个运行时创建的模块。

register首先 new Module() 生成一个模块。

// vuex/src/module/module.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // 子模块
    this._children = Object.create(null)
    // module 原始配置
    this._rawModule = rawModule
    const rawState = rawModule.state
    // state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}

实例化一个 module 后,判断当前的 path 的长度,如果为 0,就是是一个根模块,所以把 newModule 赋值给 this.root,而 new Vuex.Store() 生成的就是根模块。

如果不为 0,就建立父子模块的关系:

const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)

首先根据路径获取父模块,然后再调用父模块的 addChild 方法将子模块加入到 this._children 中,以此建立父子关系。

register 最后会检测是否有嵌套的模块,然后进行注册嵌套的模块:

if (rawModule.modules) {
  forEachValue(rawModule.modules, (rawChildModule, key) => {
    this.register(path.concat(key), rawChildModule, runtime)
  })
}

遍历当前模块定义中的所有 modules,根据 key 作为 path,递归调用 register 方法进行注册。

installModule

注册完模块就会开始安装模块:

const state = this._modules.root.state
installModule(this, state, [], this._modules.root)

installModule 方法支持 5 个参数: store,state,path(模块路径),module(根模块),hot(是否热更新)。

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,这样使得多个模块能够对同一 mutation 或 action 作出响应。但是如果有同名的 mutation 被提交,会触发所有同名的 mutation。

因此 vuex 提供了 namespaced: true 让模块成为带命名空间的模块,当模块被注册后,它的所有 action、mutation 和 getter 都会自动根据模块注册的路径调整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      // 进一步嵌套命名空间
      posts: {
        namespaced: true,
        getters: {
          popular () { ... } // -> getters['account/posts/popular']
        }
      }
    }
  }
})

回到 installModule 方法本身:

function installModule (store, rootState, path, module, hot) {
  // 是否为根模块
  const isRoot = !path.length
  // 获取命名空间,
  // 比如说 { a: moduleA } => 'a/',
  // root 没有 namespace
  const namespace = store._modules.getNamespace(path)

  // 将有 namespace 的 module 缓存起来
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  if (!isRoot && !hot) {
    // 给 store.state 添加属性,
    // 假如有 modules: { a: moduleA },
    // 则 state: { a: moduleA.state }
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // local 上下文,
  // 本质上是重写 dispatch 和 commit 函数,
  // 举个例子,modules: { a: moduleA },
  // moduleA 中有名为 increment 的 mutation,
  // 通过 makeLocalContext 函数,会将 increment 变成
  // a/increment,这样就可以在 moduleA 中找到定义的函数
  const local = module.context = makeLocalContext(store, namespace, path)

  // 下面这几个遍历循环函数,
  // 都是在注册模块中的 mutation、action 等等,
  // moduleA 中有名为 increment 的 mutation,
  // 那么会将 namespace + key 拼接起来,
  // 变为 'a/increment'
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    // 会用 this._mutations 将每个 mutation 存储起来,
    // 因为允许同一 namespacedType 对应多个方法,
    // 所以同一 namespacedType 是用数组存储的,
    // store._mutations[type] = []
    registerMutation(store, namespacedType, mutation, local)
  })

  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    // 和上面一样,用 this._actions 将 action 存储起来
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    // 会用 this._wrappedGetters 存储起来
    // getters 有一点不一样,
    // 这里保存的是一个返回 getters 的函数,
    // 而且同一 namespacedType 只能定义一个。
    registerGetter(store, namespacedType, getter, local)
  })

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

makeLocalContext

function makeLocalContext (store, namespace, path) {
  // 判断是否有 namespace
  const noNamespace = namespace === ''

  const local = {
    // 重写 dispatch
    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
      }

      return store.dispatch(type, payload)
    },

    // 重写 commit 方法
    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
      }

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

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        // local.getters 本质上是通过匹配 namespace,
        // 从 store.getters[type] 中获取
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      // local.state 本质上是通过解析 path,
      // 从 store.state 中获取
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

resetStoreVM(this, state)

初始化 store._vm,利用 Vue 将 store.state 进行响应式处理,并且将 getters 当作 Vue 的计算属性来进行处理:

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

  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍历 store._wrappedGetters 属性
  forEachValue(wrappedGetters, (fn, key) => {
    // 这里的 partial 其实就是创建一个闭包环境,
    // 保存 fn, store 两个变量,并赋值给 computed
    computed[key] = partial(fn, store)
    // 重写 get 方法,
    // store.getters.key 其实是访问了 store._vm[key],
    // 也就是去访问 computed 中的属性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 实例化一个 Vue 实例 store._vm,
  // 用它来保存 state,computed(getters),
  // 也就是利用 Vue 的进行响应式处理
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 访问 store.state 时,
  // 其实是访问了 store._vm._data.$$state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    // 这里其实就是上面的 getters
    computed
  })
  Vue.config.silent = silent

  // 开启 strict mode,
  // 只能通过 commit 的方式改变 state
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // 热重载
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    // 这里其实是动态注册模块,
    // 将新的模块内容加入后生成了新的 store._vm,
    // 然后将旧的销毁掉
    Vue.nextTick(() => oldVm.$destroy())
  }
}

partial

export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

常用 api


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) {
    // 没有 mutation 会报错并退出
    return
  }
  this._withCommit(() => {
    // 允许同一 type 下,有多个方法,
    // 所以循环数组执行 mutation,
    // 实际上执行的就是安装模块时注册的 mutation
    // handler.call(store, local.state, payload)
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  // 触发订阅了 mutation 的所有函数
  this._subscribers
    .slice()
    .forEach(sub => sub(mutation, this.state))
}

dispatch

dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    // 从 Store._actions 获取
    const entry = this._actions[type]
    if (!entry) {
            // 找不到会报错
      return
    }

    // 在 action 执行之前,
    // 触发监听了 action 的函数
    try {
      this._actionSubscribers
        .slice() 
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
        console.error(e)
    }

    // action 用 Promise.all 异步执行,
    // 实际上执行的就是安装模块时注册的 action
    /* 
    handler.call(store, {dispatch, commit, getters, state, rootGetters, rootState}, payload, cb)        
    */
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      // 在 action 执行之后,
        // 触发监听了 action 的函数
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
          console.error(e)
      }
      return res
    })
  }

watch

watch (getter, cb, options) {
  // new Vuex.Store() 时创建 _watcherVM,
  // this._watcherVM = new Vue(),
  // 本质就是调用 vue 的 api
  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}

registerModule

registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]
    
  // 注册模块
  this._modules.register(path, rawModule)
  // 安装模块
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // 重新生成 vue 实例挂载到 store 上,
  // 然后销毁旧的
  resetStoreVM(this, this.state)
}

## 备注

希望疫情尽快过去吧。——2020/02/08 元宵

以上是关于vuex源码简析的主要内容,如果未能解决你的问题,请参考以下文章

第五节:JQuery框架源码简析

第五节:JQuery框架源码简析

并发工具-CyclicBarrier源码简析

[tomcat]源码简析 结构和一次请求

Flask 启动时的源码简析

第五节:JQuery框架源码简析