Vuex 原理

背景

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。Vuex 是專門爲 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度數據響應機制來進行高效的狀態更新。若是你已經靈活運用,可是依然好奇它底層實現邏輯,不妨一探究竟。javascript

Vue 組件開發

咱們知道開發 Vue 插件,安裝的時候須要執行 Vue.use(Vuex)vue

import Vue from 'vue'
import Vuex from '../vuex'

Vue.use(Vuex)
複製代碼

經過查看 Vue API Vue-use 開發文檔,咱們知道安裝 Vue.js 插件。若是插件是一個對象,必須提供 install 方法。若是插件是一個函數,它會被做爲 install 方法。install 方法調用時,會將 Vue 做爲參數傳入。該方法須要在調用 new Vue() 以前被調用。當 install 方法被同一個插件屢次調用,插件將只會被安裝一次。java

爲了更好了的去理解源碼意思,這裏寫了一個簡單的測試實例。git

測試實例代碼

import Vue from 'vue'
import Vuex from '../vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  plugins: [],
  state: {
    time: 1,
    userInfo: {
      avatar: '',
      account_name: '',
      name: ''
    },
  },
  getters: {
    getTime (state) {
      console.log('1212',state)
      return state.time
    }
  },
  mutations: {
    updateTime(state, payload){
      state.time = payload
    }
  },
  actions: {
    operateGrou({ commit }) {
      // commit('updateTime', 100)
      return Promise.resolve().then(()=>{
        return {
          rows: [1,2,3]
        }
      })
    }
  },
  modules: {
    report: {
      namespaced: true,
      state: {
        title: '',
      },
      getters: {
        getTitle (state) {
          return state.title
        }
      },
      mutations: {
        updateTitle(state, payload){
          state.title = payload
        }
      },
      actions: {
        operateGrou({ commit }) {
          commit('updateTitle', 100)
          return Promise.resolve().then(()=>{
            return {
              rows: [1,2,2,3]
            }
          })
        }
      },
      modules: {
        reportChild: {
          namespaced: true,
          state: {
            titleChild: '',
          },
          mutations: {
            updateTitle(state, payload){
              state.title = payload
            }
          },
          actions: {
            operateGrou({ commit }) {
              commit('updateTitle', 100)
              return Promise.resolve().then(()=>{
                return {
                  rows: [1,2,2,3]
                }
              })
            }
          },
        }
      }
    },
    part: {
      namespaced: true,
      state: {
        title: '',
      },
      mutations: {
        updateTitle(state, payload){
          state.title = payload
        },
        updateTitle1(state, payload){
          state.title = payload
        }
      },
      actions: {
        operateGrou({ commit }) {
          commit('updateTitle', 100)
          return Promise.resolve().then(()=>{
            return {
              rows: [1,2,2,3]
            }
          })
        }
      },
      modules: {
        partChild: {
          namespaced: true,
          state: {
            titleChild: '',
          },
          getters: {
            getTitleChild (state) {
              return state.titleChild
            }
          },
          mutations: {
            updateTitle(state, payload){
              state.titleChild = payload
            }
          },
          actions: {
            operateGrou({ commit }) {
              commit('updateTitle', 1000)
              return Promise.resolve().then(()=>{
                return {
                  rows: [1,2,2,3]
                }
              })
            }
          },
          modules: {
            partChildChild: {
              namespaced: true,
              state: {
                titleChild: '',
              },
              getters: {
                getTitleChild (state) {
                  return state.titleChild
                }
              },
              mutations: {
                updateTitle(state, payload){
                  state.titleChild = payload
                }
              },
              actions: {
                operateGrou({ commit }) {
                  commit('updateTitle', 1000)
                  return Promise.resolve().then(()=>{
                    return {
                      rows: [1,2,2,3]
                    }
                  })
                }
              },
            }
          }
        }
      }
    }
  }
})

複製代碼

Graphviz 父子結點關係圖

用 Graphviz 圖來表示一下父子節點的關係,方便理解github

組件開發第一步 install & mixin

在調用 Vuex 的時候會找其 install 方法,並把組件實例傳遞到 install 方法的參數中。vuex

let Vue;
class Store {
	
}

const install = _Vue => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate(){
           console.log(this.$options.name);
        }
    })
};

export default {
    Store,
    install
}
複製代碼

到這裏說一下 Vuex 實現的思想,在 Vuex 的 install 方法中,能夠獲取到 Vue 實例。
咱們在每一個 Vue 實例上添加 $store 屬性,可讓每一個屬性訪問到 Vuex 數據信息;
咱們在每一個 Vue 實例的 data 屬性上添加上 state,這樣 state 就是響應式的;
收集咱們傳入 new Vuex.Store(options) 即 options 中全部的 mutaions、actions、getters;
接着當咱們 dispatch 的時候去匹配到 Store 類中存放的 actions 方法,而後去執行;
當咱們 commit 的時候去匹配到 Store 類中存放的 mutations 方法,而後去執行;
這其實就是一個發佈訂閱模式,先存起來,後邊用到再取再執行。好了解這些,咱們開始真正的源碼分析;後端

Vue 實例注入 $store

爲了更好理解,咱們打印出 Vue 實例,能夠看到注入了 $store,見下圖。
api

image.png

具體實現關鍵點數組

const install = (_Vue) => {
  Vue = _Vue
  Vue.mixin({
    beforeCreate(){
      // 咱們能夠看下面 main.js 默認只有咱們的根實例上有 store,故 this.$options.store 有值是根結點
      if(this.$options.store) { 
        this.$store = this.$options.store // 根結點賦值
      } else {
        this.$store = this.$parent && this.$parent.$store // 每一個實例都會有父親。故一層層給實例賦值
      }
    }
  })
}
複製代碼
  • main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
複製代碼

$store.state 響應式

響應式核心就是掛載到實例 data 上,讓 Vue 內部運用 Object.defineProperty 實現響應式。app

class Store{
  constructor (options) { // 咱們知道 options 是用戶傳入 new Vuex.Store(options) 參數
    this.vm = new Vue({
      data: {
        state: options.state
      }
    })
  }
}
複製代碼

$store.mutations & commit

來看下 用戶傳入的 mutations 變成了什麼,數據採用 最上面的測試實例代碼。
咱們能夠看到 mutations 是一個對象,裏面放了函數名,值是數組,將相同函數名對應的函數存放到數組中。

image.png

mutations

  • 實際上就是收集用戶傳入的 mutations, 放到一個對象中。
const setMoutations = (data, path = []) => {
    const mutations = data.mutations
    Object.keys(mutations).map(item => {
      this.mutations[item] = this.mutations[item] || [] // 以前的舊值
      this.mutations[item].push(mutations[item]) // 存起來
    })
    const otherModules = data.modules || {} // 有子 modules 則遞歸
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setMoutations(otherModules[item], path.concat(item))
      })
    }
  }
  setMoutations(options) // 這裏 options 是用戶傳入的 new Vuex.Store(options) 的參數
複製代碼

commit

  • 實際上就是從收集 mutaitons 中找到用戶傳入的mutationName對應的數組方法,而後遍歷執行。通知到位。
class Store{
	commit = (mutationName, payload) => {
    this.mutations[mutationName].map(fn => {
      fn(this.state, payload)
    })
  }
}
複製代碼

$store.actions & dispatch

actions

  • actions 與 mutations 實現是同樣的
const setAction = (data, path = []) => {
    const actions = data.actions
    Object.keys(actions).map(item => {
      this.actions[item] = this.actions[item] || []
      this.actions[item].push(actions[item])
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setAction(otherModules[item], path.concat(item))
      })
    }
  }
  setAction(options)
複製代碼

dispatch

class Store{
  dispatch = (acitonName, payload) => {
    this.actions[acitonName].map(fn => {
      fn(this, payload) // this.$store.dispatch('operateGrou')
    })
  }
}
複製代碼

$store.getters

const setGetter = (data, path = []) => {
  const getter = data.getters || {}
  const namespace = data.namespaced
  Object.keys(getter).map(item => {     
    // 跟 Vue 計算屬性底層實現相似,當從 store.getters.doneTodos 取值的時候,實際會執行 這個方法。
    Object.defineProperty(this.getter, item, { 
      get:() => {
        return options.state.getters[item](this.state)
      }
    })
  })

  const otherModules = data.modules || {}
  if (Object.keys(otherModules).length > 0){
    Object.keys(otherModules).map(item => {
      setGetter(otherModules[item], path.concat(item))
    })
  }
}
setGetter(options)
複製代碼

namespaced

上面討論的是沒有 namespaced 的狀況,加上 namespaced 有什麼區別呢,見下圖。

image.png

瞬間撥雲見日了,日常寫上面基本上都要加上 namespaced,防止命名衝突,方法重複屢次執行。
如今就算每一個 modules 的方法命同樣,也默認回加上這個方法別包圍的全部父結點的 key。
下面對 mutations actions getters 擴展一下,讓他們支持 namespaced。核心就是 path 變量

mutations

// 核心點在 path
const setMoutations = (data, path = []) => {
    const mutations = data.mutations
    const namespace = data.namespaced
    Object.keys(mutations).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item) // 將全部父親用 斜槓 相關聯
      }
      this.mutations[key] = this.mutations[key] || []
      this.mutations[key].push(mutations[item])
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setMoutations(otherModules[item], path.concat(item)) // path.concat 不會修改 path 原來的值
      })
    }
  }
  setMoutations(options)
複製代碼

actions

actions 與 mutations 是同樣的

const setAction = (data, path = []) => {
    const actions = data.actions
    const namespace = data.namespaced
    Object.keys(actions).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item)
      }
      this.actions[key] = this.actions[key] || []
      // this.actions[key].push(actions[item]) 
      this.actions[key].push((payload) => {
        actions[item](this, payload);
      })
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setAction(otherModules[item], path.concat(item))
      })
    }
  }
  setAction(options)
複製代碼

getter

const setGetter = (data, path = []) => {
    const getter = data.getters || {}
    const namespace = data.namespaced
    Object.keys(getter).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item)
      }
      Object.defineProperty(this.getter, key, {
        get: () => {
          return getter[item](this.state)
        }
      })
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setGetter(otherModules[item], path.concat(item))
      })
    }
  }
  setGetter(options)

複製代碼

咱們能夠總結來看,namespaces 加與不加的區別實際就下圖;

image.png
image.png


具體數據轉化方式有不少,核心就是對數據格式的處理,來進行發佈與訂閱;

actions & mutations

看到這,小夥伴懷疑了,actions 與 mutations,具體實現是同樣的,那爲何要說 actions 能夠異步執行,mutations,不能異步執行呢?下面我來貼一下核心代碼。

class Store{
  constructor () {
    ////..... 省略
    
  	if(this.strict){// 嚴格模式下才給報錯提示
      this.vm.$watch(()=>{
          return this.vm.state // 咱們知道 commit 是會出發 state 值修改的
      },function () {
	        // 此處監聽 state 修改,由於在執行 commit 的時候 this._committing 是true 的,你若放了異步方法,this._committing 就會往下執行 變成 false
          console.assert(this._committing,'您異步調用了!')  // 斷言 this._committing 爲false, 給報錯提示
      },{deep:true,sync:true});
    }
  }
  _withCommit(fn){
    const committing = this._committing; // 保留false
    this._committing = true; // 調用 mutation以前, this._committing 更改值是 true
    fn(); // 保證 執行的時候 this._committing 是 true
    this._committing = committing // 結束後重置爲 false
  }
  commit = (mutationName, payload) => {
    console.log('1212',mutationName)
    this._withCommit(()=>{
      this.mutations[mutationName] && this.mutations[mutationName].map(fn => {
        fn(this.state, payload)
      })
    })
  }
}
複製代碼

Vuex中的輔助方法

咱們常常在 Vuex 中這樣使用

import {
  mapState,
  mapGetters
} from 'vuex'

computed: {
  isAfterSale () {
    return this.$route.meta.isAfterSale
  },
  ...mapGetters({
    messageState: 'message/getMessageState'
  }),
  ...mapGetters({
    messageNum: 'message/getMessageNum'
  }),
 ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
    // ...
  ])
},
  
 methods: {
  ...mapActions([
    'increment', // 將 `this.increment()` 映射爲 `this.$store.dispatch('increment')`

    // `mapActions` 也支持載荷:
    'incrementBy' // 將 `this.incrementBy(amount)` 映射爲 `this.$store.dispatch('incrementBy', amount)`
  ]),
  ...mapActions({
    add: 'increment' // 將 `this.add()` 映射爲 `this.$store.dispatch('increment')`
  })
}
複製代碼

mapState

export const mapState = (stateArr) => { // {age:fn}
    let obj = {};
    stateArr.forEach(stateName => {
        obj[stateName] = function () {
            return this.$store.state[stateName]
        }
    });
    return obj;
}

複製代碼

mapGetters

export function mapGetters(gettersArr) {
  let obj = {};
  gettersArr.forEach(getterName => {
      obj[getterName] = function () {
          return this.$store.getters[getterName];
      }
  });
  return obj
}
複製代碼

mapMutations

export function mapMutations(obj) {
  let res = {};
  Object.entries(obj).forEach(([key, value]) => {
      res[key] = function (...args) {
          this.$store.commit(value, ...args)
      }
  })
  return res;
}
複製代碼

mapActions

export function mapActions(obj) {
  let res = {};
  Object.entries(obj).forEach(([key, value]) => {
      res[key] = function (...args) {
          this.$store.dispatch(value, ...args)
      }
  })
  return res;
}
複製代碼

插件

Vuex 的 store 接受 plugins 選項,這個選項暴露出每次 mutation 的鉤子。Vuex 插件就是一個函數,它接收 store 做爲惟一參數:

實際上 具體實現是發佈訂閱着模式,經過store.subscribe 將須要執行的函數保存到 store subs 中,
當 state 值發生改變時,this.subs(fn=>fn()) 執行。

const vuePersists = store => {
 	let local = localStorage.getItem('VuexStore');
  if(local){
    store.replaceState(JSON.parse(local)); // 本地有則賦值
  }
  store.subscribe((mutation,state)=>{
    localStorage.setItem('VuexStore',JSON.stringify(state)); // state 發生變化執行
  });
}
const store = new Vuex.Store({
  // ...
  plugins: [vuePersists]
})
複製代碼
class Store{
  constructor () {
		this.subs = []
		const setMoutations = (data, path = []) => {
      const mutations = data.mutations
      const namespace = data.namespaced
      Object.keys(mutations).map(mutationName => {
        let namespace = mutationName
        if (namespace) {
          namespace = path.join('/').concat('/'+mutationName)
        }
        this.mutations[namespace] = this.mutations[namespace] || []
        this.mutations[namespace].push((payload)=>{ // 以前是直接 push
        
        mutations[item](options.state, payload)
        
        this.subs.forEach(fn => fn({ // state 發生改變 則發佈通知給插件
            type: namespace,
            payload: payload
          }, options.state));
          
        })
      })
      const otherModules = data.modules || {}
      if (Object.keys(otherModules).length > 0){
        Object.keys(otherModules).map(item => {
          setMoutations(otherModules[item], path.concat(item)) // path.concat 不會修改 path 原來的值
        })
      }
    }
    setMoutations(options)
    
  }
  subscribe(fn) {
    this.subs.push(fn);
	}
}

複製代碼

State 處理

state 還沒處理呢,別忘記咱們 用戶傳入的 state 只是分module 傳的,最終都要掛載到 state 中,見初始值和下圖圖片。其實是數據格式轉化,相信跟後端對接多的同窗,考驗處理數據格式的能力了。是的遞歸跑不了了。

  • 初始值
{
 state: {
    time: 1,
    userInfo: {
      avatar: '',
      account_name: '',
      name: ''
    },
  },
  modules: {
  	report: {
      state: {
        title: '',
      },
    },
    part: {
    	state: {
      	title: '',
    	},
			modules: {
        partChild: {
          state: {
            titleChild: '',
          },
        }
      }
    },
  }
}
複製代碼
  • 轉化稱下面這種格式

image.png

能夠看到核心方法仍是 path, path.slice 來獲取每次遞歸的父結點。

const setState = (data, path = []) => {
   if (path.length > 0) {
      let parentModule = path.slice(0, -1).reduce((next, prev)=>{
        return next[prev]
      }, options.state)
      Vue.set(parentModule, path[path.length - 1], data.state); // 爲了 State 每一個屬性添加 get set 方法
    // parentModule[path[path.length - 1]] = data.state // 這樣修改 Vue 是不會監聽的
    }  
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setState(otherModules[item], path.concat(item))
      })
    }
  }
  setState(options)
複製代碼

收集模塊

原諒我,最重要的一塊放到最後才說。上面全部方法都是基於用戶傳的 options (new Vuex.Store(options)) 來實現的。可是用戶輸入的咱們怎能輕易詳細,咱們仍是要對模塊進行進一步格式處理,轉化成咱們須要的數據。轉化成見下圖。

image.png

咱們能夠分析出收集模塊,實際也是遞歸,轉化成固定格式數據 _children、state、rawModule。
直接看源碼把 核心代碼是 register 方法,實際也是數據格式的轉化。

總結

通篇看下來,仍是須要本身手敲一下,在實踐的過程當中,才能發現問題(this 指向、父子結點判斷、異步方法保存提示的巧妙)。當你明白具體實現,那每次使用就垂手可得了,每一步使用都知道幹了什麼的感受真好。

相關文章
相關標籤/搜索