vuex 基本入門和使用(二)

edwin020020020發表於2019-03-02

vuex 版本為^2.3.1,按照我自己的理解來整理vuex。

關於 state

每個vuex 應用只有一個 store 例項,所以使用起來不會太複雜,對於定位錯誤狀態和操作會很方便。

簡單用法:在vuex 的計算屬性中返回vuex 的狀態

最基本的使用方式,通過在vue檔案裡面初始化 vuex 的 store 來進行操作 vuex 的資料:如下例子:

// 在元件裡面使用 vuex
// 初始化 vuex 例項 
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }
})

// 建立一個 Counter 元件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
		// 直接返回 state
      return store.state.count
    }
  }
}

// 初始化 vue 例項
const app = new Vue({
  el: `#app`,
  components: { Counter },
  template: `
    <div class="app">
     <button @click="increment">+</button>
     <button @click="decrement">-</button> 
     <counter></counter>
    </div>
  `
  ,
   methods: {
    increment () {
      store.commit(`increment`)
    },
    decrement () {
    	store.commit(`decrement`)
    }
  }
})
複製程式碼
  • 每當 store.state.count 變化的時候, 都會重新求取計算屬性,並且觸發更新相關聯的 DOM。
  • 然而,這種模式導致元件依賴全域性狀態單例。在模組化的構建系統中,在每個需要使用 state 的元件中需要頻繁地匯入,並且在測試元件時需要模擬狀態。

我以為,當專案發展到多個模組,多個元件和子元件混合的時候,在這種場合,單純的值傳遞方式會很麻煩,因為元件或者模組之間的變數是獨立的,對於一些全域性使用的屬性類似 token,cookie 之類的東西,或者是一些多個模組之間共享的屬性。

所以 vuex 提供一個新的方式來將 vuex 的 store 存放到根元件下,通過 store 選項,將store從根元件“注入”到每一個子元件中(需呼叫 Vue.use(Vuex)):

// 初始化 vuex的 store(可以將這個放到其他檔案裡面去)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }
})

// 在初始化 vue的時候註冊 store(store 即是 vuex 的 store)
const app = new Vue({
  el: `#app`,
  // 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
  store,
  components: { Counter }, // 子元件
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})
複製程式碼

通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,且子元件能通過 this.$store 訪問到。讓我們更新下 Counter 的實現:

// 這是子元件 Counter
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
		//  通過this.$store能夠訪問到 store 並且獲取到 state
      return this.$store.state.count
    }
  }
}
複製程式碼

通過這種方式可以在實際應用中,將 vuex 的初始化分離出去其他模組化檔案,然後在 vue初始化的引用相關 vuex 的檔案即可,然後通過this.$store全域性呼叫 vuex 的 store 來實現資料儲存。

我整理了一下完整的例子,這是 jsrun的例子:
jsrun.net/qWqKp

高階用法:mapState 輔助函式

當一個元件需要獲取多個狀態時候,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,我們可以使用 mapState 輔助函式幫助我們生成計算屬性,讓你少按幾次鍵。(其實就是自動轉換了一些語法輸出)

import { mapState } from `vuex`需要先引入才可以使用

mapState使用前後對比:

// 不使用mapState時:
computed: {
    count () {
      return this.$store.state.count
    }
  }
// 使用mapState時:
computed: mapState({
	   count: state => state.count,
})
複製程式碼

如果在大批量的類似這種的計算屬性的話,使用 mapState 會更加便捷,而且不只是普通字串,函式也可以轉換,確實是比較方便的。

這裡會有一個疑問,mapState到底做了什麼事情,怎麼將程式碼轉為 vue 能識別的程式碼?所以需要參考 vuex 原始碼中關於mapState的部分:(我找了當前 vuex 版本3.01的原始碼:github.com/vuejs/vuex/…

這是normalizeMap的程式碼:

function normalizeMap (map) {
  // 判斷是否陣列,並且最終返回也是一個陣列
  return Array.isArray(map)
    // 是陣列就直接 map 迴圈
    ? map.map(key => ({ key, val: key }))
    // 是物件就將 key拿出來,然後再進行 map 迴圈
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製程式碼

這是mapState的程式碼:

var mapState = normalizeNamespace(function (namespace, states) {
  var res = {}; // 這是一個物件型別
  // 將 state 通過normalizeMap格式化變成一個陣列,陣列元素是一個物件
  normalizeMap(states).forEach(function (ref) {
    var key = ref.key;// 將陣列元素物件解出來,先儲存起來被後面使用
    var val = ref.val;
    // 組成一個新陣列,以 key 為 key,值是一個函式mappedState
    res[key] = function mappedState () {
      var state = this.$store.state; // 將本身 vuex 的 store 的 state 和 getters 儲存
      var getters = this.$store.getters;
      // 先不看 namespace 部分
      // ......
      // 這個函式裡面會判斷真正的值 ref.val 是函式還是普通值
      return typeof val === `function`
        ? val.call(this, state, getters) // 函式會被直接執行,並且傳入 vuex 的state 和 getters
        : state[val] // 值會直接放到  vuex 的state 裡面
    };
  });
  return res
});
複製程式碼
  • states引數在這裡只有2種,一種是物件{},一種是陣列[]。因為normalizeMap只做了這2個判斷。
  • states 會先通過normalizeMap進行序列化,轉換為一個陣列,陣列元素是{ key, val: key } 這種結構的。
  • 這裡需要注意一下:如果傳入的是物件裡面只有函式,如下例的countPlusLocalState,那麼被 object.keys獲取的 key 是函式的名字。
// 例如傳入的state 是一個陣列,如下:
[
   {
     count: state => state.count,   
   }
]
// 那麼被normalizeMap轉換後:
// 即轉換為{ key, val: key })
[
    { 
     key, // key 是物件{count: state => state.count}
     val: key // val是物件{count: state => state.count}
    },
    //.....
]

//------------------------

// 例如傳入的state 是一個物件,如下:
{
  count: state => state.count,
}
// 那麼被normalizeMap轉換後:
// 即被Object.keys(map).map(key => ({ key, val: map[key] }))處理後
[
    { 
     key, // key 是count,因為被Object.keys提取出來
     val: map[key] //  val 是state => state.count,這裡以 key 為鍵找物件的值
    },
    //.....
]
複製程式碼
  • normalizeMap(states)格式化之後會使用 forEach 遍歷這個陣列:
    • 如果 val 是一個函式,則直接呼叫這個 val 函式,把當前 store 上的 state 和 getters 作為引數,返回值作為 mappedState 的返回值。
    • 否則直接把 this.$store.state[val]作為 mappedState 的返回值。
// 被 foreach 遍歷,繼續用上面的例子的state來舉例,因為不是函式,所以被直接返回:
res["count"] = this.$store.state[`state => state.count`]
// 雖然這裡是以陣列的寫法,但是在 js 裡面陣列的寫法也可以用在物件上。

//如果是函式的話,會被執行val.call,並且傳入了幾個引數(this, this.$store.state, this.$store.getters)
res["countPlusLocalState"] = this.$store.state[`state => state.count`]
複製程式碼
  • 最終能夠形成一個新的陣列物件結構,並返回。
  • 因為最終生成的資料就是 computed 的資料格式,所以直接將這個物件傳給 computed 就可以直接使用了。

這是mapState經過原始碼解析前和後的對比:

// states 為物件時候,mapState轉換前
computed: mapState({
	count: state => state.count,
	// 傳字串引數 `count` 等同於 `state => state.count`
	countAlias: `count`,
	// 為了能夠使用 `this` 獲取區域性狀態,必須使用常規函式
	countPlusLocalState (state) {
  		return state.count + this.localCount
	}
})

// states 為物件時候,mapState轉換後
computed: { 
	count() { 
	   // 直接轉換為一般的計算屬性的使用方式
		return this.$store.state.count 
	},
	countAlias() { 
		// 也是轉為一般的計算屬性的使用方式,只不過有指定名字的會使用中括號括起來
		return this.$store.state[`count`]
	}, 
	countPlusLocalState() { 
		// 因為是函式,所以會被直接執行,並且傳入了當前 store 上的 state 和 getters作為引數
		//(但這裡沒使用 getters)
		return this.$store.state.count + this.localCount 
	} 
}
複製程式碼

元件仍然保有區域性狀態

使用 Vuex 並不意味著你需要將所有的狀態放入 Vuex。雖然將所有的狀態放到 Vuex 會使狀態變化更顯式和易除錯,但也會使程式碼變得冗長和不直觀。如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態。你應該根據你的應用開發需要進行權衡和確定。

關於 getter

Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性),就像計算屬性一樣,getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算。

// 初始化 getter
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: `...`, done: true },
      { id: 2, text: `...`, done: false }
    ]
  },
  getters: { // 這就是 getters
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})

// 使用getter
store.getters.doneTodos // -> [{ id: 1, text: `...`, done: true }]
// 或者可以this.$store.getters.xxxx 這樣使用。
複製程式碼
// 可以在第二個引數裡面傳一個 getter 作為引數
getters: {
  // ...
  doneTodosCount: (state, getters) => {
	  // 傳入了之前設定的doneTodos的 getters,所以直接使用了doneTodos
    return getters.doneTodos.length
  }
}

store.getters.doneTodosCount // -> 1

// 讓 getter 返回一個函式,來實現給 getter 傳參。在你對 store 裡的陣列進行查詢時非常有用。
getters: {
  // ...
  getTodoById: (state) => (id) => { // 返回一個函式
    return state.todos.find(todo => todo.id === id)
  }
}

// 對返回的函式傳入引數來使用
store.getters.getTodoById(2) // -> { id: 2, text: `...`, done: false }
複製程式碼

mapGetters 輔助函式

mapGetters 輔助函式僅僅是將 store 中的 getter 對映到區域性計算屬性。

參考 vuex 原始碼,類似的處理也是跟...mapState類似:

export function mapGetters(getters) {
    const res = {}
    // 先格式化,然後再處理
    normalizeMap(getters).forEach(({key, val}) => {
        res[key] = function mappedGetter() {
            if (!(val in this.$store.getters)) {
                console.error(`[vuex] unknown getter: ${val}`)
            }
            return this.$store.getters[val]
        }
    })
    return res
}
複製程式碼

相比 mapState 來說,他簡單一點。

唯一的區別就是它的 val 不能是函式,只能是一個字串,而且會檢查 val in this.$store.getters的值,如果為 false 會輸出一條錯誤日誌。

關於三個點…mapstate 處理

其實三個點就是es6的擴充套件運算子。

可以將一個陣列轉為用逗號分隔的引數序列,也可以將物件進行展開,如果應用到 mapstate 身上就是:

需要注意:vue的 computed 是物件,裡面的屬性是物件屬性。

computed: {
  // 一般 computed 物件屬性
  now: function () {
    return Date.now()
  }
  // 使用物件展開運算子將此物件混入到外部物件中
  ...mapGetters([
      `doneTodosCount`,
      `anotherGetter`,
      // ...
    ])

  // 轉換後是這樣,跟一般 computed 物件屬性差不多
  doneTodosCount:function(){},
  anotherGetter:function(){}
}

複製程式碼

參考:

相關文章