Vue源码 Vuex(三)Vuex API实现

学习内容和文章内容来自 黄轶老师
《Vue.js2.0 源码揭秘》
《Vue.js 3.0 核心源码解析》
这里分析的源码是Runtime + Compiler 的 Vue.js
调试代码在:node_modules\vue\dist\vue.esm.js 里添加
vue版本:Vue.js 2.5.17-beta

你越是认真生活,你的生活就会越美好——弗兰克·劳埃德·莱特
《人生果实》经典语录

Vue源码学习完整目录

API

上一节我们对 Vuex 的初始化过程有了深入的分析,在我们构造好这个 store 后,需要提供一些 API 对这个 store 做存取的操作,那么这一节我们就从源码的角度对这些 API 做分析。

数据获取

Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 ab,我们可以通过 store.state.a.b.xxx 的方式去获取。它的实现是在发生在 installModule 的时候:

function installModule (store, rootState, path, module, hot) {
    
    
  const isRoot = !path.length
  
  // ...
  // set state
  if (!isRoot && !hot) {
    
    
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
    
    
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // ...
}

在递归执行 installModule 的过程中,就完成了整个 state 的建设,这样我们就可以通过 module 名的 path 去访问到一个深层 modulestate

有些时候,我们获取的数据不仅仅是一个 state,而是由多个 state 计算而来,Vuex 提供了 getters,允许我们定义一个 getter 函数,如下:

getters: {
    
    
  total (state, getters, localState, localGetters) {
    
    
    // 可访问全局 state 和 getters,以及如果是在 modules 下面,可以访问到局部 state 和 局部 getters
    return state.a + state.b
  }
}

我们在 installModule 的过程中,递归执行了所有 getters 定义的注册,在之后的 resetStoreVM 过程中,执行了 store.getters 的初始化工作:

function installModule (store, rootState, path, module, hot) {
    
    
  // ...
  const namespace = store._modules.getNamespace(path)
  // ...
  const local = module.context = makeLocalContext(store, namespace, path)

  // ...

  module.forEachGetter((getter, key) => {
    
    
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // ...
}

function registerGetter (store, type, rawGetter, local) {
    
    
  if (store._wrappedGetters[type]) {
    
    
    if (process.env.NODE_ENV !== 'production') {
    
    
      console.error(`[vuex] duplicate getter key: ${
      
      type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    
    
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}


function resetStoreVM (store, state, hot) {
    
    
  // ...
  // bind store public getters
  store.getters = {
    
    }
  const wrappedGetters = store._wrappedGetters
  const computed = {
    
    }
  forEachValue(wrappedGetters, (fn, key) => {
    
    
    // use computed to leverage its lazy-caching mechanism
    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
  // ...
  store._vm = new Vue({
    
    
    data: {
    
    
      $$state: state
    },
    computed
  })
  // ...
}

installModule 的过程中,为建立了每个模块的上下文环境,
因此当我们访问 store.getters.xxx 的时候,实际上就是执行了 rawGetter(local.state,...)

rawGetter 就是我们定义的 getter 方法,这也就是为什么我们的 getter 函数支持这四个参数,并且除了全局的 stategetter 外,我们还可以访问到当前 module 下的 stategetter

数据存储

Vuex 对数据存储的存储本质上就是对 state 做修改,并且只允许我们通过提交 mutaion 的形式去修改 statemutation 是一个函数,如下:

mutations: {
    
    
  increment (state) {
    
    
    state.count++
  }
}

mutations 的初始化也是在 installModule 的时候:

function installModule (store, rootState, path, module, hot) {
    
    
  // ...
  const namespace = store._modules.getNamespace(path)

  // ...
  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachMutation((mutation, key) => {
    
    
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // ...
}

function registerMutation (store, type, handler, local) {
    
    
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    
    
    handler.call(store, local.state, payload)
  })
}

store 提供了commit 方法让我们提交一个 mutation

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'
    )
  }
}

这里传入的 _type 就是 mutationtype,我们可以从 store._mutations 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedMutationHandler(playload)

接着会执行我们定义的 mutation 函数,并传入当前模块的 state,所以我们的 mutation 函数也就是对当前模块的 state 做修改。

需要注意的是, mutation 必须是同步函数,但是我们在开发实际项目中,经常会遇到要先去发送一个请求,然后根据请求的结果去修改 state,那么单纯只通过 mutation 是无法完成需求,因此 Vuex 又给我们设计了一个 action 的概念。

action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。例如:

mutations: {
    
    
  increment (state) {
    
    
    state.count++
  }
},
actions: {
    
    
  increment (context) {
    
    
    setTimeout(() => {
    
    
      context.commit('increment')
    }, 0)
  }
}

actions 的初始化也是在 installModule 的时候:

function installModule (store, rootState, path, module, hot) {
    
    
  // ...
  const namespace = store._modules.getNamespace(path)

  // ...
  const local = module.context = makeLocalContext(store, namespace, path)

  module.forEachAction((action, key) => {
    
    
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
}  )
  // ...
}

function registerAction (store, type, handler, local) {
    
    
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    
    
    let res = handler.call(store, {
    
    
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    if (!isPromise(res)) {
    
    
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
    
    
      return res.catch(err => {
    
    
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
    
    
      return res
    }
  })
}

store 提供了dispatch 方法让我们提交一个 action

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
  }

  this._actionSubscribers.forEach(sub => sub(action, this.state))

  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}

这里传入的 _type 就是 actiontype,我们可以从 store._actions 找到对应的函数数组,遍历它们执行获取到每个 handler 然后执行,实际上就是执行了 wrappedActionHandler(payload)

接着会执行我们定义的 action 函数,并传入一个对象,包含了当前模块下的 dispatchcommitgettersstate,以及全局的 rootStaterootGetters,所以我们定义的 action 函数能拿到当前模块下的 commit 方法。

因此 action 比我们自己写一个函数执行异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的一些方法和状态,Vuex 帮我们做好了这些。

语法糖

我们知道 storeStore 对象的一个实例,它是一个原生的 Javascript 对象,我们可以在任意地方使用它们。

但大部分的使用场景还是在组件中使用,那么我们之前介绍过,在 Vuex 安装阶段,它会往每一个组件实例上混入 beforeCreate 钩子函数,然后往组件实例上添加一个 $store 的实例,它指向的就是我们实例化的 store,因此我们可以在组件中访问到 store 的任何属性和方法。

比如我们在组件中访问 state

const Counter = {
    
    
  template: `<div>{
     
     { count }}</div>`,
  computed: {
    
    
    count () {
    
    
      return this.$store.state.count
    }
  }
}

但是当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。同样这些问题也在存于 gettermutationaction

为了解决这个问题,Vuex 提供了一系列 mapXXX 辅助函数帮助我们实现在组件中可以很方便的注入 store 的属性和方法。

mapState

我们先来看一下 mapState 的用法:

// 在单独构建的版本中辅助函数为 Vuex.mapState
import {
    
     mapState } from 'vuex'

export default {
    
    
  // ...
  computed: mapState({
    
    
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
    
    
      return state.count + this.localCount
    }
  })
}

再来看一下 mapState 方法的定义,在 src/helpers.js 中:

export const mapState = normalizeNamespace((namespace, states) => {
    
    
  const res = {
    
    }
  normalizeMap(states).forEach(({
     
      key, val }) => {
    
    
    res[key] = function mappedState () {
    
    
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
    
    
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
    
    
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

function normalizeNamespace (fn) {
    
    
  return (namespace, map) => {
    
    
    if (typeof namespace !== 'string') {
    
    
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
    
    
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

function normalizeMap (map) {
    
    
  return Array.isArray(map)
    ? map.map(key => ({
    
     key, val: key }))
    : Object.keys(map).map(key => ({
    
     key, val: map[key] }))
}

首先 mapState 是通过执行 normalizeNamespace 返回的函数,它接收 2 个参数,其中 namespace 表示命名空间,map 表示具体的对象,namespace 可不传,稍后我们来介绍 namespace 的作用。

当执行 mapState(map) 函数的时候,实际上就是执行 normalizeNamespace 包裹的函数,然后把 map 作为参数 states 传入。

mapState 最终是要构造一个对象,每个对象的元素都是一个方法,因为这个对象是要扩展到组件的 computed 计算属性中的。

函数首先执行 normalizeMap 方法,把这个 states 变成一个数组,数组的每个元素都是 {key, val} 的形式。

接着再遍历这个数组,以 key 作为对象的 key,值为一个 mappedState 的函数,在这个函数的内部,获取到 $store.getters$store.state

然后再判断数组的 val 如果是一个函数,执行该函数,传入 stategetters,否则直接访问 state[val]

比起一个个手动声明计算属性,mapState 确实要方便许多,下面我们来看一下 namespace 的作用。

当我们想访问一个子模块的 state 的时候,我们可能需要这样访问:

computed: {
    
    
  mapState({
    
    
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},

这样从写法上就很不友好,mapState 支持传入 namespace, 因此我们可以这么写:

computed: {
    
    
  mapState('some/nested/module', {
    
    
    a: state => state.a,
    b: state => state.b
  })
},

这样看起来就清爽许多。在 mapState 的实现中,如果有 namespace,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的 module,然后把 stategetters 修改为 module 对应的 stategetters

function getModuleByNamespace (store, helper, namespace) {
    
    
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    
    
    console.error(`[vuex] module namespace not found in ${
      
      helper}(): ${
      
      namespace}`)
  }
  return module
}

我们在 Vuex 初始化执行 installModule 的过程中,初始化了这个映射表:

function installModule (store, rootState, path, module, hot) {
    
    
  // ...
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    
    
    store._modulesNamespaceMap[namespace] = module
  }

  // ...
}

mapGetters

我们先来看一下 mapGetters 的用法:

import {
    
     mapGetters } from 'vuex'

export default {
    
    
  // ...
  computed: {
    
    
    // 使用对象展开运算符将 getter 混入 computed 对象中
    mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性,来看一下它的定义:

export const mapGetters = normalizeNamespace((namespace, getters) => {
    
    
  const res = {
    
    }
  normalizeMap(getters).forEach(({
     
      key, val }) => {
    
    
    // thie namespace has been mutate by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
    
    
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
    
    
        return
      }
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
    
    
        console.error(`[vuex] unknown getter: ${
      
      val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]

mapMutations

我们可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 的调用。

我们先来看一下 mapMutations 的用法:

import {
    
     mapMutations } from 'vuex'

export default {
    
    
  // ...
  methods: {
    
    
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
    
    
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的 methods 映射为 store.commit 的调用。来看一下它的定义:

export const mapMutations = normalizeNamespace((namespace, mutations) => {
    
    
  const res = {
    
    }
  normalizeMap(mutations).forEach(({
     
      key, val }) => {
    
    
    res[key] = function mappedMutation (...args) {
    
    
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
    
    
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
    
    
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

可以看到 mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutationpayload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 modulecommit 上。

mapActions

我们可以在组件中使用 this.$store.dispatch('xxx') 提交 action,或者使用 mapActions 辅助函数将组件中的 methods 映射为 store.dispatch 的调用。

mapActions 在用法上和 mapMutations 几乎一样,实现也很类似:

export const mapActions = normalizeNamespace((namespace, actions) => {
    
    
  const res = {
    
    }
  normalizeMap(actions).forEach(({
     
      key, val }) => {
    
    
    res[key] = function mappedAction (...args) {
    
    
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
    
    
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
    
    
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

mapMutations 的实现几乎一样,不同的是把 commit 方法换成了 dispatch

动态更新模块

在 Vuex 初始化阶段我们构造了模块树,初始化了模块上各个部分。

在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。

registerModule (path, rawModule, options = {
     
     }) {
    
    
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    
    
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0, 'cannot register the root module by using registerModule.')
  }

  this._modules.register(path, rawModule)
  installModule(this, this.state, path, this._modules.get(path), options.preserveState)
  // reset store to update getters...
  resetStoreVM(this, this.state)
}

registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm

相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。

unregisterModule (path) {
    
    
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    
    
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }

  this._modules.unregister(path)
  this._withCommit(() => {
    
    
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)
}

unregisterModule 支持传入一个 path 模块路径,首先执行 unregister 方法去修剪我们的模块树:

unregister (path) {
    
    
  const parent = this.get(path.slice(0, -1))
  const key = path[path.length - 1]
  if (!parent.getChild(key).runtime) return

  parent.removeChild(key)
}

注意,这里只会移除我们运行时动态创建的模块

接着会删除 state 在该路径下的引用,最后执行 resetStore 方法:

function resetStore (store, hot) {
    
    
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

该方法就是把 store 下的对应存储的 _actions_mutations_wrappedGetters_modulesNamespaceMap 都清空,然后重新执行 installModule 安装所有模块以及 resetStoreVM 重置 store._vm

总结

那么至此,Vuex 提供的一些常用 API 我们就分析完了,包括数据的存取、语法糖、模块的动态更新等。

要理解 Vuex 提供这些 API 都是方便我们在对 store 做各种操作来完成各种能力,尤其是 mapXXX 的设计,让我们在使用 API 的时候更加方便,这也是我们今后在设计一些 JavaScript 库的时候,从 API 设计角度中应该学习的方向。

Vue源码学习目录

Vue源码学习完整目录


谢谢你阅读到了最后~
期待你关注、收藏、评论、点赞~
让我们一起 变得更强

猜你喜欢

转载自blog.csdn.net/weixin_42752574/article/details/122656628