稍微學一下 Vuex 原理

博客原文html

介紹

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式
這種集中管理應用狀態的模式相比父子組件通訊來講,使數據的通訊更方便,狀態的更改也更加直觀。前端

Bus

確定有很多同窗在寫 Vue 時使用過 new Vue() 建立 bus 進行數據通訊。vue

import Vue from 'vue';
const bus = new Vue();
export default {
  install(Vue) {
    Object.defineProperty(Vue.prototype, '$bus', {
      get () { return bus }
    });
  }
};

組件中使用 this.$bus.$on this.$bus.$emit 監聽和觸發 bus 事件進行通訊。
bus 的通訊是不依賴組件的父子關係的,所以實際上能夠理解爲最簡單的一種狀態管理模式。
經過 new Vue() 能夠註冊響應式的數據,
下面基於此對 bus 進行改造,實現一個最基本的狀態管理:git

// /src/vuex/bus.js
let Vue
// 導出一個 Store 類,一個 install 方法
class Store {
  constructor (options) {
    // 將 options.state 註冊爲響應式數據
    this._bus = new Vue({
      data: {
        state: options.state
      }
    })
  }
  // 定義 state 屬性
  get state() {
    return this._bus._data.state;
  }
}
function install (_Vue) {
  Vue = _Vue
  // 全局混入 beforeCreate 鉤子
  Vue.mixin({
    beforeCreate () {
      // 存在 $options.store 則爲根組件
      if (this.$options.store) {
        // $options.store 就是建立根組件時傳入的 store 實例,直接掛在 vue 原型對象上
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}
export default {
  Store,
  install
}

建立並導出 store 實例:github

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex/bus'
Vue.use(Vuex) // 調用 Vuex.install 方法
export default new Vuex.Store({
  state: {
    count: 0
  }
})

建立根組件並傳入 store 實例:vuex

// /src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

組件中使用示例:數組

<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
  </div>
</template>
<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.state.count++
    }
  }
}
</script>

從零實現一個 Vuex

前一節經過 new Vue() 定義一個響應式屬性並經過 minxin 爲全部組件混入 beforeCreate 生命週期鉤子函數的方法爲每一個組件內添加 $store 屬性指向根組件的 store 實例的方式,實現了最基本的狀態管理。
繼續這個思路,下面從零一步步實現一個最基本的 Vuex。app

如下代碼的 git 地址: simple-vuex

總體結構

let Vue;
class Store {}
function install() {}
export default {
  Store,
  install
}

install 函數

// 執行 Vue.use(Vuex) 時調用 並傳入 Vue 類
// 做用是爲全部 vue 組件內部添加 `$store` 屬性
function install(_Vue) {
  // 避免重複安裝
  if (Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
    }
    return
  }
  Vue = _Vue; // 暫存 Vue 用於其餘地方有用到 Vue 上的方法
  Vue.mixin({
    // 全局全部組件混入 beforeCreate 鉤子,給每一個組件中添加 $store 屬性指向 store 實例
    beforeCreate: function vuexInit() {
      const options = this.$options;
      if (options.store) {
        // 接收參數有=中有 store 屬性則爲根組件
        this.$store = options.store;
      } else if (options.parent && options.parent.$store) {
        // 非根組件經過 parent 父組件獲取
        this.$store = options.parent.$store;
      }
    }
  })
}

Store 類

// 執行 new Vuex.Store({}) 時調用
class Store {
  constructor(options = {}) {
    // 初始化 getters mutations actions
    this.getters = {};
    this._mutations = {};
    this._actions = {};
    // 給每一個 module 註冊 _children 屬性指向子 module
    // 用於後面 installModule 中根據 _children 屬性查找子 module 進行遞歸處理
    this._modules = new ModuleCollection(options)
    const { dispatch, commit } = this;
    // 固定 commit dispatch 的 this 指向 Store 實例
    this.commit = (type, payload) => {
      return commit.call(this, type, payload);
    }
    this.dispatch = (type, payload) => {
      return dispatch.call(this, type, payload);
    }
    // 經過 new Vue 定義響應式 state
    const state = options.state;
    this._vm = new Vue({
      data: {
        state: state
      }
    });
    // 註冊 getters  mutations actions
    // 並根據 _children 屬性對子 module 遞歸執行 installModule
    installModule(this, state, [], this._modules.root);
  }
  // 定義 state commit dispatch
  get state() {
    return this._vm._data.state;
  }
  set state(v){
    throw new Error('[Vuex] vuex root state is read only.')
  }
  commit(type, payload) {
    return this._mutations[type].forEach(handler => handler(payload));
  }
  dispatch(type, payload) {
    return this._actions[type].forEach(handler => handler(payload));
  }
}

ModuleCollection 類

Store 類的構造函數中初始化 _modules 時是經過調用 ModuleCollection 這個類,內部從根模塊開始遞歸遍歷 modules 屬性,初始化模塊的 _children 屬性指向子模塊。frontend

class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule)
  }
  // 遞歸註冊,path 是記錄 module 的數組 初始爲 []
  register(path, rawModule) {
    const newModule = {
      _children: {},
      _rawModule: rawModule,
      state: rawModule.state
    }
    if (path.length === 0) {
      this.root = newModule;
    } else {
      // 非最外層路由經過 reduce 從 this.root 開始遍歷找到父級路由
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module._children[key];
      }, this.root);
      // 給父級路由添加 _children 屬性指向該路由
      parent._children[path[path.length - 1]] = newModule;
      // 父級路由 state 中也添加該路由的 state
      Vue.set(parent.state, path[path.length - 1], newModule.state);
    }
    // 若是當前 module 還有 module 屬性則遍歷該屬性並拼接 path 進行遞歸
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule);
      })
    }
  }
}

installModule

Store 類的構造函數中調用 installModule ,經過 _modules 的 _children 屬性遍歷到每一個模塊並註冊 getters mutations actionsasync

function installModule(store, rootState, path, module) {
  if (path.length > 0) {
    const parentState = rootState;
    const moduleName = path[path.length - 1];
    // 全部子模塊都將 state 添加到根模塊的 state 上
    Vue.set(parentState, moduleName, module.state)
  }
  const context = {
    dispatch: store.dispatch,
    commit: store.commit,
  }
  // 註冊 getters mutations actions
  const local = Object.defineProperties(context, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => {
        let state = store.state;
        return path.length ? path.reduce((state, key) => state[key], state) : state
      }
    }
  })
  if (module._rawModule.actions) {
    forEachValue(module._rawModule.actions, (actionFn, actionName) => {
      registerAction(store, actionName, actionFn, local);
    });
  }
  if (module._rawModule.getters) {
    forEachValue(module._rawModule.getters, (getterFn, getterName) => {
      registerGetter(store, getterName, getterFn, local);
    });
  }
  if (module._rawModule.mutations) {
    forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
      registerMutation(store, mutationName, mutationFn, local)
    });
  }
  // 根據 _children 拼接 path 並遞歸遍歷
  forEachValue(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

installModule 中用來註冊 getters mutations actions 的函數:

// 給 store 實例的 _mutations 屬性填充
function registerMutation(store, mutationName, mutationFn, local) {
  const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []);
  entry.push((payload) => {
    mutationFn.call(store, local.state, payload);
  });
}

// 給 store 實例的 _actions 屬性填充
function registerAction(store, actionName, actionFn, local) {
  const entry = store._actions[actionName] || (store._actions[actionName] = [])
  entry.push((payload) => {
    return actionFn.call(store, {
      commit: local.commit,
      state: local.state,
    }, payload)
  });
}

// 給 store 實例的 getters 屬性填充
function registerGetter(store, getterName, getterFn, local) {
  Object.defineProperty(store.getters, getterName, {
    get: () => {
      return getterFn(
        local.state,
        local.getters,
        store.state
      )
    }
  })
}

// 將對象中的每個值放入到傳入的函數中做爲參數執行
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key));
}

使用

還有 modules、plugins 等功能尚未實現,並且 getters 的並無使用 Vue 的 computed 而只是簡單的以函數的形式實現,可是已經基本完成了 Vuex 的主要功能,下面是一個使用示例:

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    changeCount(state, payload) {
      console.log('changeCount', payload)
      state.count += payload;
    }
  },
  actions: {
    asyncChangeCount(ctx, payload) {
      console.log('asyncChangeCount', payload)
      setTimeout(() => {
        ctx.commit('changeCount', payload);
      }, 500);
    }
  }
})
<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
    <button @click="asyncChangeCount">async +1</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.commit('changeCount', 1);
    },
    asyncChangeCount() {
      this.$store.dispatch('asyncChangeCount', 1);
    }
  },
  mounted() {
    console.log(this.$store)
  }
}
</script>
閱讀源碼的過程當中寫了一些方便理解的註釋,但願給你們閱讀源碼帶來幫助,github: vuex 源碼

參考

相關文章
相關標籤/搜索