美文网首页纵横研究院VU...
Vuex实现TodoList及部分源码分析

Vuex实现TodoList及部分源码分析

作者: 雩风二十八夜 | 来源:发表于2019-11-02 16:24 被阅读0次

    1.安装Vuex

    npm install vuex --save
    

    2.在src下创建store文件夹,再分别创建index.js actions.js mutations.js文件

    • index.js中引入vuevuex
    • 再引入actions.jsmutations.js文件
    • 导出默认出口
    //  index.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    import mutations from './mutations'
    import actions from './actions'
    
    //使用vuex
    Vue.use(Vuex)
    
    //创建vuex实例
    export default new Vuex.Store({
        state: {
            todos: []
        },
        actions,
        mutations,
    })
    

    3.在main.js中引入文件,在vue实例全局引入store实例

    //  main.js
    import Vue from 'vue'
    import App from './App.vue'
    import store from './store'
    
    new Vue({
      store,  //把store对象注入所有的子组件
      render: h => h(App),
    }).$mount('#app')
    
    

    TodoList实现

    state

    Vuex使用单一状态树,每个应用仅包含一个store实例。其中state包含全部的应用状态,因此将todos存储在state中,通过this.$store.state.todos访问

    mutation

    要想更改store中状态的唯一方法是提交mutation,每个mutation都有一个字符串的事件类型 (type) 和 一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受state作为第一个参数,需要对状态进行改变时可传入第二个参数:

    export const mutations = {
        addTodo  (state, todo) {
            state.todos.push(todo);
        },
        removeTodo (state, todo) {
            state.todos.splice(state.todos.indexOf(todo), 1)
        },
        editTodo (state, { todo, text = todo.text, done = todo.done }) {
            todo.text = text
            todo.done = done
        }
    }
    

    通过store.commit触发,或者使用mapMutations辅助函数将组件中的methods映射为store.commit调用(需要在根节点注入store):

    this.$store.commit("addTodo", todo);
    this.$store.commit("removeTodo", todo);
    this.$store.commit("editTodo", {todo, text:value, done:!todo.done});
    
    action

    action类似于mutation,区别是:

    • mutation是修改state的唯一途径,action提交mutation,但不能直接修改state
    • mutation必须同步执行,action可以进行异步操作
      注册简单的action
    export default {
      addTodo ({ commit }, text) {
        commit('addTodo', {
          text,
          done: false
        })
      },
      removeTodo ({ commit }, todo) {
        commit('removeTodo', todo)
      },
      changeTodo ({ commit }, todo) {
        commit('editTodo', { todo, done: !todo.done })
      },
      editTodo ({ commit }, { todo, value }) {
        commit('editTodo', { todo, text: value })
      },
    }
    

    action通过store.dispatch触发,或者使用mapActions辅助函数将组件的methods映射为store.dispatch调用(需要先在根节点注入store):

    methods: {
        ...mapActions([
          'editTodo',
          'changeTodo',
          'removeTodo'
        ]),
        doneEdit (e) {
          const value = e.target.value.trim()
          const { todo } = this
          if (!value) {
            this.removeTodo(todo)
          } else if (this.editing) {
            this.editTodo({
              todo,
              value
            })
            this.editing = false
          }
        },
        ······
      }
    

    Vuex工作流可参考如下图片:

    Vuex流程图

    Vuex官方文档: Vuex 是什么?

    辅助函数源码分析

    为了避免每次都需要通过this.$store来调用api,vuex提供了mapState mapMutations mapGetters mapActions createNamespaceHelpers 等api。具体实现存放在src/helper.js中:

    • 一些工具函数

    下面这些函数都是实现以上提到的 api 所用到的:

    /**
     * 统一数据格式,将数组或对象展现成如下格式
     * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
     * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
     * @param {Array|Object} map
     * @return {Object}
     */
    function normalizeMap (map) {
      return Array.isArray(map)
        ? map.map(key => ({ key, val: key }))
        : Object.keys(map).map(key => ({ key, val: map[key] }))  
          //  Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
    }
    
    /**
     * 返回一个函数,参数分别为namespace和map,判断是否存在namespace,统一进行namespace处理
     * @param {Function} fn
     * @return {Function}
     */
    function normalizeNamespace (fn) {
      return (namespace, map) => {
        if (typeof namespace !== 'string') {
          map = namespace
          namespace = ''
        } else if (namespace.charAt(namespace.length - 1) !== '/') {
        //  charAt() 方法可返回指定位置的字符
          namespace += '/'
        }
        return fn(namespace, map)
      }
    }
    
    /**
     * 通过namespace获取module
     * @param {Object} store
     * @param {String} helper
     * @param {String} namespace
     * @return {Object}
     */
    function getModuleByNamespace (store, helper, namespace) {
      //  _modulesNamespaceMap参考src/store.js
      const module = store._modulesNamespaceMap[namespace]
      if (process.env.NODE_ENV !== 'production' && !module) {
        console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
      }
      return module
    }
    
    • mapState

    为组件创建计算属性返回store中的状态:

    /**
     * 减少在vue.js中获取state的代码
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} states # 对象的项可以是一个接收state和getter的参数的函数,你可以在其中对state和getter做一些事情。
     * @return {Object}
     */
    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) {
            //  获取module
            const module = getModuleByNamespace(this.$store, 'mapState', namespace)
            if (!module) {
              return
            }
            //  获取module的state和getters
            state = module.context.state
            getters = module.context.getters
          }
          //  Object类型的val是函数,传递过去的参数是state和getters
          return typeof val === 'function'
            ? val.call(this, state, getters)
            : state[val]
        }
        // mark vuex getter for devtools
        res[key].vuex = true
      })
      return res
    })
    

    mapStatenormalizeNamespace的返回值。首先利用normalizeMapstates进行格式的统一,然后遍历,对参数的所有state包裹一层函数,返回一个对象。

    • mapGetters
    /**
     * 减少获取getters的代码
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} getters
     * @return {Object}
     */
    export const mapGetters = normalizeNamespace((namespace, getters) => {
      const res = {}
      normalizeMap(getters).forEach(({ key, val }) => {
        // The namespace has been mutated 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
    })
    

    同样的处理方式,遍历getters,只是这里需要加上命名空间,这是因为在注册时_wrapGetters中的getters是有加上命名空间的

    • mapMutations

    创建组件方法提交mutation

    /**
     * 减少提交mutation的代码
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} mutations # 对象的项可以是一个接受'commit '函数作为第一个参数的函数,它还可以接受另一个参数。你可以在这个函数中提交变异和做任何其他事情。特别是,您需要从映射函数传递另一个参数
     * @return {Object}
     */
    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
          }
           //  执行mutation
          return typeof val === 'function'
            ? val.apply(this, [commit].concat(args))
            : commit.apply(this.$store, [val].concat(args))
        }
      })
      return res
    })
    

    判断是否存在namespace后,commit是不一样的,每个module都是保存了上下文的,这里如果存在namespace就需要使用那个另外处理的commit等信息

    • mapActions
    /**
     * 减少派发action 的代码
     * @param {String} [namespace] - Module's namespace
     * @param {Object|Array} actions # 对象的项可以是一个接受“派发”函数作为第一个参数的函数,它还可以接受另一个参数。可以在这个函数中调度action并执行其他任何操作。
     * @return {Object}
     */
    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的处理方式相似

    辅助函数的主要目的是减少代码量,通过各类api直接在文件中调用函数改变状态,不需要通过this.$store一步步进行操作。

    相关文章

      网友评论

        本文标题:Vuex实现TodoList及部分源码分析

        本文链接:https://www.haomeiwen.com/subject/ggttvctx.html