Vuex實(shí)現(xiàn)TodoList及部分源碼分析

1.安裝Vuex

npm install vuex --save

2.在src下創(chuàng)建store文件夾,再分別創(chuàng)建index.js actions.js mutations.js文件

  • index.js中引入vuevuex
  • 再引入actions.js,mutations.js文件
  • 導(dǎo)出默認(rèn)出口
//  index.js
import Vue from 'vue'
import Vuex from 'vuex'

import mutations from './mutations'
import actions from './actions'

//使用vuex
Vue.use(Vuex)

//創(chuàng)建vuex實(shí)例
export default new Vuex.Store({
    state: {
        todos: []
    },
    actions,
    mutations,
})

3.在main.js中引入文件,在vue實(shí)例全局引入store實(shí)例

//  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實(shí)現(xiàn)

state

Vuex使用單一狀態(tài)樹,每個(gè)應(yīng)用僅包含一個(gè)store實(shí)例。其中state包含全部的應(yīng)用狀態(tài),因此將todos存儲在state中,通過this.$store.state.todos訪問

mutation

要想更改store中狀態(tài)的唯一方法是提交mutation,每個(gè)mutation都有一個(gè)字符串的事件類型 (type) 和 一個(gè)回調(diào)函數(shù) (handler)。這個(gè)回調(diào)函數(shù)就是我們實(shí)際進(jìn)行狀態(tài)更改的地方,并且它會接受state作為第一個(gè)參數(shù),需要對狀態(tài)進(jìn)行改變時(shí)可傳入第二個(gè)參數(shù):

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觸發(fā),或者使用mapMutations輔助函數(shù)將組件中的methods映射為store.commit調(diào)用(需要在根節(jié)點(diǎn)注入store):

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

action類似于mutation,區(qū)別是:

  • mutation是修改state的唯一途徑,action提交mutation,但不能直接修改state
  • mutation必須同步執(zhí)行,action可以進(jìn)行異步操作
    注冊簡單的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觸發(fā),或者使用mapActions輔助函數(shù)將組件的methods映射為store.dispatch調(diào)用(需要先在根節(jié)點(diǎn)注入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 是什么?

輔助函數(shù)源碼分析

為了避免每次都需要通過this.$store來調(diào)用api,vuex提供了mapState mapMutations mapGetters mapActions createNamespaceHelpers 等api。具體實(shí)現(xiàn)存放在src/helper.js中:

  • 一些工具函數(shù)

下面這些函數(shù)都是實(shí)現(xiàn)以上提到的 api 所用到的:

/**
 * 統(tǒng)一數(shù)據(jù)格式,將數(shù)組或?qū)ο笳宫F(xiàn)成如下格式
 * 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() 方法會返回一個(gè)由一個(gè)給定對象的自身可枚舉屬性組成的數(shù)組
}

/**
 * 返回一個(gè)函數(shù),參數(shù)分別為namespace和map,判斷是否存在namespace,統(tǒng)一進(jìn)行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

為組件創(chuàng)建計(jì)算屬性返回store中的狀態(tài):

/**
 * 減少在vue.js中獲取state的代碼
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} states # 對象的項(xiàng)可以是一個(gè)接收state和getter的參數(shù)的函數(shù),你可以在其中對state和getter做一些事情。
 * @return {Object}
 */
export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  //  統(tǒng)一數(shù)組格式并遍歷數(shù)組
  normalizeMap(states).forEach(({ key, val }) => {
    //  返回一個(gè)對象,值都是函數(shù)
    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是函數(shù),傳遞過去的參數(shù)是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進(jìn)行格式的統(tǒng)一,然后遍歷,對參數(shù)的所有state包裹一層函數(shù),返回一個(gè)對象。

  • 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,只是這里需要加上命名空間,這是因?yàn)樵谧詴r(shí)_wrapGetters中的getters是有加上命名空間的

  • mapMutations

創(chuàng)建組件方法提交mutation

/**
 * 減少提交mutation的代碼
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} mutations # 對象的項(xiàng)可以是一個(gè)接受'commit '函數(shù)作為第一個(gè)參數(shù)的函數(shù),它還可以接受另一個(gè)參數(shù)。你可以在這個(gè)函數(shù)中提交變異和做任何其他事情。特別是,您需要從映射函數(shù)傳遞另一個(gè)參數(shù)
 * @return {Object}
 */
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    //  返回一個(gè)對象,值是函數(shù)
    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
      }
       //  執(zhí)行mutation
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

判斷是否存在namespace后,commit是不一樣的,每個(gè)module都是保存了上下文的,這里如果存在namespace就需要使用那個(gè)另外處理的commit等信息

  • mapActions
/**
 * 減少派發(fā)action 的代碼
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} actions # 對象的項(xiàng)可以是一個(gè)接受“派發(fā)”函數(shù)作為第一個(gè)參數(shù)的函數(shù),它還可以接受另一個(gè)參數(shù)??梢栽谶@個(gè)函數(shù)中調(diào)度action并執(zhí)行其他任何操作。
 * @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的處理方式相似

輔助函數(shù)的主要目的是減少代碼量,通過各類api直接在文件中調(diào)用函數(shù)改變狀態(tài),不需要通過this.$store一步步進(jìn)行操作。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容