Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。Vuex 是專門爲 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度數據響應機制來進行高效的狀態更新。若是你已經靈活運用,可是依然好奇它底層實現邏輯,不妨一探究竟。javascript
咱們知道開發 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 圖來表示一下父子節點的關係,方便理解github
在調用 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,見下圖。
api
具體實現關鍵點數組
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 // 每一個實例都會有父親。故一層層給實例賦值
}
}
})
}
複製代碼
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')
複製代碼
響應式核心就是掛載到實例 data 上,讓 Vue 內部運用 Object.defineProperty 實現響應式。app
class Store{
constructor (options) { // 咱們知道 options 是用戶傳入 new Vuex.Store(options) 參數
this.vm = new Vue({
data: {
state: options.state
}
})
}
}
複製代碼
來看下 用戶傳入的 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) 的參數
複製代碼
class Store{
commit = (mutationName, payload) => {
this.mutations[mutationName].map(fn => {
fn(this.state, payload)
})
}
}
複製代碼
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)
複製代碼
class Store{
dispatch = (acitonName, payload) => {
this.actions[acitonName].map(fn => {
fn(this, payload) // this.$store.dispatch('operateGrou')
})
}
}
複製代碼
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,防止命名衝突,方法重複屢次執行。
如今就算每一個 modules 的方法命同樣,也默認回加上這個方法別包圍的全部父結點的 key。
下面對 mutations actions getters 擴展一下,讓他們支持 namespaced。核心就是 path 變量
// 核心點在 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 與 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)
複製代碼
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 加與不加的區別實際就下圖;
看到這,小夥伴懷疑了,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 中這樣使用
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')`
})
}
複製代碼
export const mapState = (stateArr) => { // {age:fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function () {
return this.$store.state[stateName]
}
});
return obj;
}
複製代碼
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function () {
return this.$store.getters[getterName];
}
});
return obj
}
複製代碼
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function (...args) {
this.$store.commit(value, ...args)
}
})
return res;
}
複製代碼
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 只是分module 傳的,最終都要掛載到 state 中,見初始值和下圖圖片。其實是數據格式轉化,相信跟後端對接多的同窗,考驗處理數據格式的能力了。是的遞歸跑不了了。
{
state: {
time: 1,
userInfo: {
avatar: '',
account_name: '',
name: ''
},
},
modules: {
report: {
state: {
title: '',
},
},
part: {
state: {
title: '',
},
modules: {
partChild: {
state: {
titleChild: '',
},
}
}
},
}
}
複製代碼
能夠看到核心方法仍是 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)) 來實現的。可是用戶輸入的咱們怎能輕易詳細,咱們仍是要對模塊進行進一步格式處理,轉化成咱們須要的數據。轉化成見下圖。
咱們能夠分析出收集模塊,實際也是遞歸,轉化成固定格式數據 _children、state、rawModule。
直接看源碼把 核心代碼是 register 方法,實際也是數據格式的轉化。
通篇看下來,仍是須要本身手敲一下,在實踐的過程當中,才能發現問題(this 指向、父子結點判斷、異步方法保存提示的巧妙)。當你明白具體實現,那每次使用就垂手可得了,每一步使用都知道幹了什麼的感受真好。