實現簡易的 Vuex

What

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態只能經過可預測的方式改變。查看官網html

Why

多組件共享同一個狀態時,會依賴同一狀態 單一數據流沒法知足需求:vue

  • 深度嵌套組件級屬性傳值,會變得很是麻煩
  • 同級(兄弟)組件間傳值也行不通
  • 經過事件或父子組件直接引用也很繁瑣

最終致使代碼維護困難。es6

multiple components that share a common state: 多組件共享狀態 以下圖: vuex

vuex

  • 可追蹤狀態改變
  • 可維護
  • 可進行 time travel

Vue.js 官網文檔寫得很詳細,建議初學者必定要把文檔至少過一遍。數據庫

這裏附上文檔地址 Vuexapi

When

  • 當構建一個大型的 SPA 時
  • 多組件共享一個狀態

Implement

解析源碼以前,至少要對 Vuex 很是熟悉,再進一步實現。數組

新建 store 倉庫文件

在初始化 store.js 文件時,須要手動註冊 Vuex,這一步是爲了將 store 屬性注入到每一個組件的實例上,可經過 this.$store.state 獲取共享狀態。數據結構

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
複製代碼

Tip: 這裏 Vue 是在進行訂閱,Vuex.install 函數,根組件在實例化時,會自動派發執行 install 方法,並將 Vue 做爲參數傳入。異步

導出 Store 實例,傳入屬性對象,可包含如下屬性:async

  • state: 根狀態
  • getters: 依賴狀態計算出派生狀態
  • mutations: 經過 commit
  • commit 改變狀態的惟一方式,使得狀態變化可追蹤,傳入 payload 做爲第二參數
  • actions: 相似於 mutation,內部執行 commit,異步請求和操做放這裏執行,外部觸發調用 dispatch
  • dispatch 方法,傳入 payload 做爲第二個參數
  • modules: 子模塊狀態
  • namespaced: 字模塊設置命名空間
  • strict: 嚴格模式
export default new Vuex.Store({
  state: {
    todos: [
      { id: 0, done: true, text: 'Vue.js' },
      { id: 1, done: false, text: 'Vuex' },
      { id: 2, done: false, text: 'Vue-router' },
      { id: 3, done: false, text: 'Node.js' },
    ],
  },
  getters: {
   doneTodosCount(state) {
    return state.todos.filter(todo => todo.done).length;
   },
  },
  mutations: {
    syncTodoDone(state, id) {
      state.todos.forEach((todo, index) => index===id && (todo.done = true))
    }
  },
  actions: {
    asyncChange({commit}, payload) {
      setTimeout(() => commit('syncChange', payload), 1000);
    }
  },
  modules: {
    a: {
      state: {
        age: '18'
      },
      mutations: {
        syncAgeIncrement(state) {
          state.age += 1; // 這裏的 state 爲當前子模塊狀態
        }
      }
    }
  }
})
store.state.a.age // -> "18"
複製代碼

組件中使用

引入相關映射函數

import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';
複製代碼

組件中使用時,這四種映射函數用法差很少,都是返回一個對象。

方式一:

{
  computed: {
    todos() {
      return this.$store.state.todos;
    },
    age() {
      return this.$store.state.a.age
    },
    // ...
  }
}
複製代碼

Tip: 上述方式在獲取多個狀態時,代碼重複過多且麻煩,this.$store.state,可進一步優化爲第二種方式

方式二:

{
  computed: mapState({
    todos: state => state.age,
    
    // or
    todos: 'todos', // 屬性名爲別名

    // or
    todos(state) {
      return state.todos; // 內部可獲取 this 獲取當前實例數據
    }
  })
}
複製代碼

能夠看到上述的狀態映射,調用時可傳入 options 對象,屬性值有三種形式:

  • 箭頭函數(簡潔)
  • 字符串(別名)
  • normal 函數(this 指向向前組件實例)

Tip: 若是當前實例組件,有本身的私有的計算屬性時,可以使用 es6 語法的 Object Spread Operator 對象展開運算符

方式三:

{
  computed: {
    ...mapState([
      'todos',
    ]),
    otherValue: 'other value'
  }
}
複製代碼

在子模塊狀態屬性中添加 namespaced: true 字段時,mapMutations, mapActions 須要添加對應的命名空間 方式一:

{
  methods: {
    syncTodoDone(payload) {
      this.$store.commit('syncTodoDone', payload)
    },
    syncAgeIncrement() {
      this.$store.commit('syncAgeIncrement')
    }
  }
}
複製代碼

方式二:

{
  methods: {
    ...mapMutations([
      "syncTodoDone",
    ]),
    ...mapMutations('a', [
      "asyncIncrement"
    ]),
  }
}
複製代碼

方式三: 藉助 vuex 內部幫助函數進行包裝

import { createNamespacedHelpers } from 'vuex'

const { mapMutations } = createNamespacedHelpers('a')

{
  methods: mapMutations([
    'syncAgeIncrement'
  ])
}
複製代碼

除了以上基礎用法以外,還有 plugins, registerModule 屬性與 api, 後續的源碼分析上會嘗試實現。

Build

接下來開始構建一個簡易的 vuex

Application Structure 目錄結構

└── vuex
    └── src
        ├── index.js
        ├── store.js              # core code including install, Store
        └── helpers               # helper functions including mapState, mapGetters, mapMutations, mapActions, createNamespacedHelpers
        └── util.js
        ├── plugins
        │   └── logger.js
        └── module
            ├── module-collection.js
            └── module.js
複製代碼

核心入口文件

導出包含核心代碼的對象

// index.js
import { Store, install } from './store';
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers';

export default {
  Store,
  install,
  mapState,
  mapGetters,
  mapMutations,
  mapActions,
  createNamespacedHelpers
}

export {
  Store,
  install,
  mapState,
  mapGetters,
  mapMutations,
  mapActions,
  createNamespacedHelpers
}
複製代碼

store.js

實現 install 方法

function install(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        this.$store = this.$options.store;
      } else {
        this.$store = this.$parent && this.$parent.$store;
      }
    },
  })
}
複製代碼

Tip: 內部經過調用 Vue.mixin(),爲全部組件注入 $store 屬性

實現 Store 類

Store 數據結構

interface StoreOptions<S> {
    state?: S | (() => S); getters?: GetterTree<S, S>; actions?: ActionTree<S, S>; mutations?: MutationTree<S>; modules?: ModuleTree<S>; plugins?: Plugin<S>[]; strict?: boolean; } export declare class Store<S> { constructor(options: StoreOptions<S>); readonly state: S; readonly getters: any; replaceState(state: S): void; dispatch: Dispatch; commit: Commit; subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any): () => void;

  registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
  registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;
  
}
複製代碼

Tip: 以上對源碼上有必定出入,簡化以後有些屬性和方法有所刪減和改動

依次執行步驟:

  • 腳本引入時,確保 install 方法被調用,先判斷是否掛載了全局屬性 Vue
    constructor(options = {}) {
        if (!Vue && typeof Window !== undefined && Window.Vue) {
          install(Vue);
        }
    }
    複製代碼

構造函數內部先初始化實例屬性和方法

this.strict = options.strict || false;
  this._committing = false;
  this.vm = new Vue({
    data: {
      state: options.state,
    },
  });
  this.getters = Object.create(null);
  this.mutations = Object.create(null);
  this.actions = Object.create(null);
  this.subs = [];
複製代碼

Tip: this.vm 是核心,實現狀態響應式,一旦發生改變,依賴的組件視圖就會當即更新。

getters

類型爲 GetterTree 調用 Object.create(null) 建立一個乾淨的對象,即原型鏈指向 null,沒有原型對象的方法和屬性,提升性能

export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>;
}
複製代碼
  • mutations: MutationTree
export interface MutationTree<S> {
  [key: string]: Mutation<S>;
}
複製代碼
actions

類型爲 ActionTree

export interface ActionTree<S, R> {
  [key: string]: Action<S, R>;
}
複製代碼
modules

考慮到 state 對象下可能會有多個 modules,建立 ModuleCollection 格式化成想要的數據結構

export interface Module<S, R> {
  namespaced?: boolean;
  state?: S | (() => S); getters?: GetterTree<S, R>; actions?: ActionTree<S, R>; mutations?: MutationTree<S>; modules?: ModuleTree<R>; } export interface ModuleTree<R> { [key: string]: Module<any, R>; } 複製代碼
get state

core 這裏進行了依賴收集,將用戶傳入的 state 變爲響應式數據,數據變化觸發依賴的頁面更新

get state() {
  return this.vm.state;
}
複製代碼
subscribe

訂閱的事件在每一次 mutation 時發佈

subscribe(fn) {
    this.subs.push(fn);
  }
複製代碼
  • replaceState
replaceState(newState) {
    this._withCommit(() => {
      this.vm.state = newState;
    })
  }
複製代碼
subs

訂閱事件的存儲隊列

plugins
const plugins= options.plugins;
  plugins.forEach(plugin => plugin(this));
複製代碼

結構爲數組,暴露每一次 mutation 的鉤子函數。每個 Vuex 插件僅僅是一個函數,接收 惟一的參數 store,插件功能就是在mutation 調用時增長邏輯,如: - createLogger vuex/dist/logger 修改日誌(內置) - stateSnapShot 生成狀態快照 - persists 數據持久化

const persists = store => {
  // mock data from server db
  let local = localStorage.getItem('Vuex:state');
  if (local) {
    store.replaceState(JSON.parse(local));
  }
  // mock 每一次數據變了以後就往數據庫裏存數據
  store.subscribe((mutation, state) =>  localStorage.setItem('Vuex:state', JSON.stringify(state)))
}
複製代碼
_committing

boolean 監聽異步邏輯是否在 dispatch 調用

_withCommit

函數接片,劫持mutation(commit) 觸發函數。

_withCommit(fn) {
  const committing = this._committing;
  this._committing = true;
  fn();
  this._committing = committing;
}
複製代碼
strict

源碼中,在嚴格模式下,會深度監聽狀態異步邏輯的調用機制是否符合規範

if (this.strict) {
      this.vm.$watch(
        () => this.vm.state,
        function() {
          console.assert(this._committing, '不能異步調用')
        },
        {
          deep: true,
          sync: true,
        }
      );
    }
複製代碼

Tip: 生產環境下須要禁用 strict 模式,深度監聽會消耗性能,核心是調用 Vue 的監聽函數

commit
commit = (type, payload) => {
  this._withCommit(() => {
    this.mutations[type].forEach(fn => fn(payload));
  })
}
複製代碼
dispatch
dispatch = (type, payload) => {
  this.actions[type].forEach(fn => fn(payload));
}
複製代碼
registerModule 動態註冊狀態模塊
registerModule(moduleName, module) {
  this._committing = true;
  if (!Array.isArray(moduleName)) {
    moduleName = [moduleName];
  }

  this.modules.register(moduleName, module);
  installModule(this, this.state, moduleName, module.rawModule)
}
複製代碼
installModule

工具方法,註冊格式化後的數據,具體表現爲: (註冊)

/** * * @param {StoreOption} store 狀態實例 * @param {state} rootState 根狀態 * @param {Array<String>} path 父子模塊名構成的數組 * @param {Object} rawModule 當前模塊狀態對應格式化後的數據:{ state, _raw, _children, state } 其中 _raw 是 options: { namespaced?, state, getter, mutations, actions, modules?, plugins, strict} */
function installModule(store, rootState, path, rawModule) {
  
  let { getters, mutations, actions } = rawModule._raw;
  let root = store.modules.root;
複製代碼
  • 子模塊命名空間處理
const namespace = path.reduce((str, currentModuleName) => {
    // root._raw 對應的就是 當前模塊的 option, 根模塊沒有 namespaced 屬性跳過
    root = root._children[currentModuleName];
    return str + (root._raw.namespaced ? currentModuleName + '/' : '')
  }, '');
複製代碼
  • 註冊 state
if (path.length > 0) {
      let parentState = path.slice(0, -1).reduce((root, current) => root[current], rootState);
      // CORE:動態給跟狀態添加新屬性,需調用 Vue.set API,添加依賴
      Vue.set(parentState, path[path.length - 1], rawModule.state);
    }
複製代碼
  • 註冊 getters
if (getters) {
    foreach(getters, (type, fn) => Object.defineProperty(store.getters, namespace + type, {
      get: () => fn(getState(store, path))
    }))
  }
複製代碼
  • 註冊 mutations
if (mutations) {
    foreach(mutations, (type, fn) => {
      let arr = store.mutations[namespace + type] || (store.mutations[namespace + type] = []);
      arr.push(payload => {
        fn(getState(store, path), payload);
        // 發佈 subscribe 訂閱的回調函數
        store.subs.forEach(sub => sub({
          type: namespace + type,
          payload,
        }, store.state));
      })
    })
  }
複製代碼
  • 註冊 actions
if (actions) {
    foreach(actions, (type, fn) => {
      let arr = store.actions[namespace + type] || (store.actions[namespace + type] = []);
      arr.push(payload => fn(store, payload));
    })
  }
複製代碼
  • 遞歸處理子模塊
foreach(rawModule._children, (moduleName, rawModule) => installModule(store, rootState, path.concat(moduleName), rawModule))
}
複製代碼

ModuleCollection 類結構

constructor 構造函數

class ModuleCollection{
  constructor(options) {
    this.register([], options);
  }
複製代碼

register 實例方法

// rootModule: 爲當前模塊下的 StoreOption
    register(path, rootModuleOption) {
      let rawModule = {
        _raw: rootModuleOption,
        _children: Object.create(null),
        state: rootModuleOption.state
      }
      rootModuleOption.rawModule = rawModule;

      if (!this.root) {
        this.root = rawModule;
      } else {
        // 若 modules: a.modules.b => [a, b] => root._children.a._children.b
        let parentModule = path.slice(0, -1).reduce((root, current) => root._children[current], this.root);
        parentModule._children[path[path.length - 1]] = rawModule
      }

      if (rootModuleOption.modules) {
        foreach(rootModuleOption.modules, (moduleName, moduleOption) => this.register(path.concat(moduleName), moduleOption));
      }
    }
  }
複製代碼

helpers.js

幫助文件中的四個函數都是經過接受對應要映射爲對象的參數名,直接供組件內部使用。

mapState

export const mapState = (options) => {
  let obj = Object.create(null);
  if (Array.isArray(options)) {
    options.forEach((stateName) => {
      obj[stateName] = function() {
        return this.$store.state[stateName];
      };
    });
  } else {
    Object.entries(options).forEach(([stateName, value]) => {
      obj[stateName] = function() {
        if (typeof value === "string") {
          return this.$store.state[stateName];
        }
        return value(this.$store.state);
      }
    });
  }
  return obj;
};
複製代碼

參數 options 類型能夠是: - Array[string], 如:[ 'count', 'list' ] - Object, key 值爲狀態名,value 能夠是 string, arrow function, normal function,其中常規函數,能夠在內部訪問到當前組件實例

mapGetters

export function mapGetters(namespace, options) {
    let obj = Object.create(null);
    if (Array.isArray(namespace)) {
      options = namespace;
      namespace = '';
    } else {
      namespace += '/';
    }
    options.forEach(getterName => {
      console.log(getterName)
      obj[getterName] = function() {
        return this.$store.getters[namespace + getterName];
      }
    })
    return obj;
  }
複製代碼

mapMutations

export function mapMutations(namespace, options) {
  let obj = Object.create(null);
  if (Array.isArray(namespace)) {
    options = namespace;
    namespace = '';
  } else {
    namespace += '/';
  }
  options.forEach(mutationName => {
    obj[mutationName] = function(payload) {
      return this.$store.commit(namespace + mutationName, payload)
    }
  })
  return obj;
}
複製代碼

mapActions

export function mapActions(namespace, options) {
  let obj = Object.create(null);
  if (Array.isArray(namespace)) {
    options = namespace;
    namespace = '';
  } else {
    namespace += '/';
  }
  options.forEach(actionName => {
    obj[actionName] = function(payload) {
      return this.$store.dispatch(namespace + actionName, payload)
    }
  })
  return obj;
}
複製代碼

以上後三個方法包含了子模塊命名空間,參數解析以下:

參數1可選值,爲子狀態模塊的命名空間 參數2爲選項屬性,類型同 mapState

工具函數

foreach

處理對象鍵值對迭代函數處理

getState

同步用戶調用 replaceState 後,狀態內部的新狀態

const foreach = (obj, callback) => Object.entries(obj).forEach(([key, value]) => callback(key, value));
const getState = (store, path) => path.reduce((newState, current) => newState[current], store.state);
複製代碼

至此, vuex 源碼的我的分析基本完結,由於是簡版與源碼會有必定的出入。 Feel free to tell me if there is any problem.

相關文章
相關標籤/搜索