vuex原理以及實現

vuex官方文檔

Vuex是什么?

Vuex 是一個專為 Vue.js 應用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應用的所有組件的狀態(tài),并以相應的規(guī)則保證狀態(tài)以一種可預測的方式發(fā)生變化

每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含著你的應用中大部分的狀態(tài) (state)。Vuex 和單純的全局對象有以下兩點不同:

  1. Vuex 的狀態(tài)存儲是響應式的。當 Vue 組件從 store 中讀取狀態(tài)的時候,若 store 中的狀態(tài)發(fā)生變化,那么相應的組件也會相應地得到高效更新。
  2. 你不能直接改變 store 中的狀態(tài)。改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態(tài)的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

實現簡易版的vuex

先來看下vuex的基本用法

import Vue from 'vue';
import Vuex from 'vuex';
// 1.Vue.use(Vuex);  Vuex是一個對象 install方法
// 2.Vuex中有一個Store類 
// 3.混入到組件中 增添store屬性

Vue.use(Vuex); // 使用這個插件  內部會調用Vuex中的install方法

const store = new Vuex.Store({
  state:{ // -> data
    age:10
  },
  getters:{ // 計算屬性
    myAge(state){
      return state.age + 20
    }
  },
  mutations:{ // method=> 同步的更改state  mutation的參數是狀態(tài)
    changeAge(state,payload){
      state.age += payload; // 更新age屬性
    }
  },
  actions:{ // 異步操作做完后將結果提交給mutations
    changeAge({commit},payload){
      setTimeout(() => {
        commit('changeAge',payload)
      }, 1000);
    }
  }
});
export default store;

通過用法可以知道:

  1. Vuex是一個對象,它作為vue的插件,必然有install方法;
  2. Vuex中有一個Store類,在使用的時候有使用new;
  3. 需要將store混入到組件中。

于是可以梳理好入口文件

vuex/index.js

import { Store, install } from './store'; 

// 這個文件是入口文件,核心就是導出所有寫好的方法
export default {
  Store,
  install
}

store文件

vuex/store.js

export let Vue;

export class Store {
  
}

// _vue 是Vue的構造函數
export const install = (_vue) => {
  // 需要保存Vue,用戶傳入的Vue構造函數
  Vue = _vue; 
}

接下來就是把store掛載到每個組件上面,這樣數據才能互通共享,很顯然,通過Vue.mixin 在Vue生命周期beforeCreate 可以為每個組件注入store;

import applyMixin from "./mixin";
export let Vue;

export class Store {
  
}

// _vue 是Vue的構造函數
export const install = (_vue) => {
  // 需要保存Vue,用戶傳入的Vue構造函數
  Vue = _vue;
  // 需要將根組件中注入的store 分派給每一個組件 (子組件) Vue.mixin
  applyMixin(Vue);
}

vuex/mixin.js

export default function applyMixin(Vue) {
  // 父子組件的beforecreate執(zhí)行順序
  Vue.mixin({ // 內部會把生命周期函數 拍平成一個數組 
    beforeCreate: vuexInit
  });
}

// 組件渲染時從父=》子

function vuexInit() {
  // 給所有的組件增加$store 屬性 指向我們創(chuàng)建的store實例
  const options = this.$options; // 獲取用戶所有的選項
  if (options.store) { // 根實例(只有根實例才會有store屬性)
    this.$store = options.store;
  } else if (options.parent && options.parent.$store) { // 兒子 或者孫子....
    // 后面的每一個都從其父組件拿到store
    this.$store = options.parent.$store;
  }
}

接下來就是處理state,getters,mutations,actions

state實現

export class Store {
  constructor(options) {
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      }
    })
  }
  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }
  
}

首先來處理state,options是用戶傳入的,其中有state,getters,mutations,actions,自然可以在options.state中取到,但是此時state還不是響應式,可以借助new Vue中data的數據是響應式處理這個問題,將state掛載到$$state上,這個屬性是不會被vue暴露出去(可能是內部做了處理)。當我們在組件中去獲取值的時候,比如this.store.state.age時候 this.store.state 就走到到了訪問器get state() 就會將整個倉庫的state返回出去,而且數據是響應式的。至于為什么在_vm._data上,需要去看下vue源碼實現。

getters實現

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      }
    })
    
    // 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)
    this.getters = {};
    Object.key(options.getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => options.getters[key](this.state)
      })
    })
  }
  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

通過循環(huán)用戶傳進來的getters,再通過Object.defineProperty把每一個getter放入store中。不過目前每一次取值都會重新計算,沒有緩存功能,不符合vue計算屬性的用法以及定義。

先來改造下對象遍歷這個方法,因為這個方法后面用的比較多。

vuex/util.js

export const forEachValue = (obj, callback) => {
  Object.keys(obj).forEach(key => callback(obj[key], key))
}

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      }
    })

    // 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)
    this.getters = {};
    forEachValue(options.getters, (fn, key) => {
      Object.defineProperty(this.getters, key, {
        get: () => fn(this.state)
      })
    })
  }
  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

邏輯都是一樣的,接著處理下緩存功能。

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    const computed = {};

    // 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)
    this.getters = {};
    forEachValue(options.getters, (fn, key) => {
      // 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存
      computed[key] = () => fn(this.state);
      // 當取值的時候執(zhí)行計算屬性的邏輯,此時就有緩存功能
      Object.defineProperty(this.getters, key, {
        get: () => fn(this._vm[key])
      })
    })

    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      },
      computed,
    })

  }
  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

computed具有緩存功能,可以在用戶傳入的getters的時候,將用戶的getters 定義在實例上,computed[key] = () => fn(this.state) ,在取值的時候fn(this._vm[key])執(zhí)行計算屬性的邏輯。vuex的作者真是腦洞大開,鬼才啊,這都能想到。

mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)

  • 對傳入的屬性進行遍歷訂閱

  • 通過commit方法觸發(fā)調用。

mutation實現

// 3.實現mutations
this.mutations = {};
forEachValue(options.mutations, (fn, key) => {
  this.mutations[key] = (payload) => fn(this.state, payload)
})

commit = (type, payload) => { //保證當前this 當前store實例
    this.mutations[type](payload)
}

commit使用箭頭函數是為了保證調用的都是當前實例,一是通過this.commit(type,data),二是在action中被解構使用changeAge({commit},payload){}

actions和dispath也是如此。

完整的Store類

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    const computed = {};

    // 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)
    this.getters = {};
    forEachValue(options.getters, (fn, key) => {
      // 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存
      computed[key] = () => fn(this.state);
      // 當取值的時候執(zhí)行計算屬性的邏輯,此時就有緩存功能
      Object.defineProperty(this.getters, key, {
        get: () => fn(this._vm[key])
      })
    })

    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      },
      computed,
    })

    // 3.實現mutations
    this.mutations = {};
    forEachValue(options.mutations, (fn, key) => {
      this.mutations[key] = (payload) => fn(this.state, payload)
    })

    // 4.實現actions
    this.actions = {};
    forEachValue(options.actions, (fn, key) => {
      this.actions[key] = (payload) => fn(this, payload);
    });

  }

  commit = (type, payload) => { //保證當前this 當前store實例
    this.mutations[type](payload)
  }
  
  dispatch = (type, payload) => {
    this.mutations[type](payload)
  }

  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

完整的store.js

import applyMixin from "./mixin";
import { forEachValue } from './util';
export let Vue;

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    const computed = {};

    // 2.處理getters屬性 具有緩存的 computed 帶有緩存 (多次取值是如果值不變是不會重新取值)
    this.getters = {};
    forEachValue(options.getters, (fn, key) => {
      // 將用戶的getters 定義在實例上, 計算屬性是如何實現緩存
      computed[key] = () => fn(this.state);
      // 當取值的時候執(zhí)行計算屬性的邏輯,此時就有緩存功能
      Object.defineProperty(this.getters, key, {
        get: () => fn(this._vm[key])
      })
    })

    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認不會將這個屬性掛載到vm上
        $$store: state
      },
      computed,
    })

    // 3.實現mutations
    this.mutations = {};
    forEachValue(options.mutations, (fn, key) => {
      this.mutations[key] = (payload) => fn(this.state, payload)
    })

    // 4.實現actions
    this.actions = {};
    forEachValue(options.actions, (fn, key) => {
      this.actions[key] = (payload) => fn(this, payload);
    });

  }

  commit = (type, payload) => { //保證當前this 當前store實例
    this.mutations[type](payload)
  }
  
  dispatch = (type, payload) => {
    this.mutations[type](payload)
  }

  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

// _vue 是Vue的構造函數
export const install = (_vue) => {
  // 需要保存Vue,用戶傳入的Vue構造函數
  Vue = _vue;
  // 需要將根組件中注入的store 分派給每一個組件 (子組件) Vue.mixin
  applyMixin(Vue);
}

簡易版的vuex到此完成。接下來就是要處理module。

完整版Vuex實現

我們實現了一個簡易版的Vuex,對state,actions,mutations,getters 進行了功能的實現。但是沒有對modules進行處理,其實modules才是Vuex中最核心并且是最難實現的。

Vuex 允許我們將 store 分割成大大小小的對象,每個對象也都擁有自己的 state、getter、mutation、action,這個對象我們把它叫做 module(模塊),在模塊中還可以繼續(xù)嵌套子模塊。

  • state: 所有模塊中的state中數據最終都會嵌套在一棵樹上。類似于如下
image
  • 模塊內部的 action、mutation 和 getter 默認可是注冊在全局命名空間的,這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。因此在訂閱mutation 和action時必須存儲在數組中,每次觸發(fā),數組中的方法都要執(zhí)行。

    image

Vuex中可以為每個模塊添加namespaced: true來標記為當前模塊劃分一個命名空間,接下來看下具體怎么實現一個完整的Vuex。

具體實現

總體思路可以分為以下:

  1. 模塊收集。就是把用戶傳給store的數據進行格式化,格式化成我們想要的結構(樹)
  2. 安裝模塊。需要將子模塊通過模塊名定義在跟模塊上
  3. 把狀態(tài)state和getters定義到當前的vm上。

模塊收集

import ModuleCollection from './module/module-collection'
export let Vue;

export class Store {
  constructor(options) {
    
    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)

    // 1.模塊收集
    this._modules = new ModuleCollection(options);

  }
}

ModuleCollection 類的實現

這個類是收集模塊,格式化數據用的,那我們先要知道需要什么樣的格式。

this.root = {
   _raw: '根模塊',
    _children:{
      a:{
          _raw:"a模塊",
          _children:{
              c:{
                  .....
              }
          },
          state:'a的狀態(tài)'  
        },
        b:{
          _raw:"b模塊",
          _children:{},
         state:'b的狀態(tài)'  
        }
    },
    state:'根模塊自己的狀態(tài)'
 }

最終需要的是這樣一個數結構。

export default class ModuleCollection {
  constructor(options) {
    // 注冊模塊 需要用到棧結構數據,[根,a],每次循環(huán)遞歸的時候將其入棧。這樣每個模塊可以清楚的知道自己的父級是誰
    this.register([], options)
  }

  
  register(path, rootModule) {
    // 格式化后的結果
    let newModule = { 
      _raw: rootModule, // 用戶定義的模塊
      _children: {}, // 模塊的兒子
      state: {} // 當前模塊的狀態(tài)
    }
    
    
    if (path.length === 0) { // 說明是根模塊
      this.root = newModule
    } 
    
    // 用戶在模塊中傳了modules屬性
    if (rootModule.modules) {
      // 循環(huán)模塊 module模塊的定義 moduleName模塊的名字
      forEachValue(rootModule.modules, (module, moduleName) => {
        this.register(path.concat(moduleName), module)
      })
    }
    
  }
}

第一次進來的時候path是空數組,root就是用戶傳進去的模塊對象;如果模塊有modules屬性,需要循環(huán)去注冊這個模塊。path.concat(moduleName) 就返回了[a,c]類似的格式。 接下來看下path不為空的時候

if (path.length === 0) { // 說明是根模塊
  this.root = newModule
} else {
  // this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位
  this.root._children[path[path.length -1]] = newModule
}

path[path.length -1] 可以取到最后一項,也就是模塊的兒子模塊。這里我們用的是this.root._children[path[path.length -1]] = newModule。這樣寫會把有多層路徑的模塊最后一項也提到和它平級,因此需要確定這個模塊的父級是誰,再把當前模塊掛到父級就okl了

if (path.length === 0) { // 說明是根模塊
  this.root = newModule
} else {
  // this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位
  // path.splice(0, -1) 是最后一項,是需要被掛的模塊
  let parent = path.splice(0, -1).reduce((memo, current) => {
    return memo._children[current];
  }, this.root);
  parent._children[path[path.length - 1]] = newModule
}

模塊的安裝

將所有module收集后需要對收集到數據進行整理

  • state數據要合并。 通過Vue.set(parent,path[path.length-1],rootModule.state),既可以合并,又能使使 module數據成為響應式數據;
  • action 和mutation 中方法訂閱(數組)
// 1.模塊收集
this._modules = new ModuleCollection(options);

// 2.安裝模塊 根模塊的狀態(tài)中 要將子模塊通過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

this就是store, 需要完成installModule方法。installModule中傳入的有當前模塊,這個模塊可能有自己的方法。為此先改造下代碼,創(chuàng)建Module類。

import { forEachValue } from '../util';

class Module {
  get namespaced() {
    return !!this._raw.namespaced
  }

  constructor(newModule) {
    this._raw = newModule;
    this._children = {};
    this.state = newModule.state
  }

  getChild(key) {
    return this._children[key];
  }

  addChild(key, module) {
    this._children[key] = module
  }

  // 給模塊繼續(xù)擴展方法
  
}

export default Module;

ModuleCollection中相應的地方稍作修改。

import Module from './module'

export default class ModuleCollection {
  constructor(options) {
    // 注冊模塊 需要用到棧結構數據,[根,a],每次循環(huán)遞歸的時候將其入棧。這樣每個模塊可以清楚的知道自己的父級是誰
    this.register([], options)
  }

  register(path, rootModule) {
    // 格式化后的結果
    let newModule = new Module(rootModule)

    if (path.length === 0) { // 說明是根模塊
      this.root = newModule
    } else {
      // this.register(path.concat(moduleName), module); 遞歸注冊前會把module 的名放在 path的位
      // path.splice(0, -1) 是最后一項,是需要被掛的模塊
      let parent = path.splice(0, -1).reduce((memo, current) => {
        return memo.getChild(current);
      }, this.root);
      parent.addChild(path[path.length - 1], newModule)
    }

    // 用戶在模塊中傳了modules屬性
    if (rootModule.modules) {
      // 循環(huán)模塊 module模塊的定義 moduleName模塊的名字
      forEachValue(rootModule.modules, (module, moduleName) => {
        this.register(path.concat(moduleName), module)
      })
    }

  }
}
function installModule(store, rootState, path, module) {
  // 這里我需要遍歷當前模塊上的 actions、mutation、getters 都把他定義在store的_actions, _mutations, _wrappedGetters 中
  
}

installModule 就需要循環(huán)對當前模塊處理對應的actions、mutation、getters。為此可以對Module類增加方法,來讓其內部自己處理。

import { forEachValue } from '../util';

class Module {

  constructor(newModule) {
    this._raw = newModule;
    this._children = {};
    this.state = newModule.state
  }

  getChild(key) {
    return this._children[key];
  }

  addChild(key, module) {
    this._children[key] = module
  }

  // 給模塊繼續(xù)擴展方法
  forEachMutation(fn) {
    if (this._raw.mutations) {
      forEachValue(this._raw.mutations, fn)
    }
  }

  forEachAction(fn) {
    if (this._raw.actions) {
      forEachValue(this._raw.actions, fn);
    }
  }

  forEachGetter(fn) {
    if (this._raw.getters) {
      forEachValue(this._raw.getters, fn);
    }
  }

  forEachChild(fn) {
    forEachValue(this._children, fn);
  }
}

export default Module;
function installModule(store, rootState, path, module) {
  // 這里我需要遍歷當前模塊上的 actions、mutation、getters 都把他定義在store的_actions, _mutations, _wrappedGetters 中

  // 處理mutation
  module.forEachMutation((mutation, key) => {
    store._mutations[key] = (store._mutations[key] || [])
    store._mutations[key].push((payload) => {
      mutation.call(store, module.state, payload)
    })
  })

  // 處理action
  module.forEachAction((action, key) => {
    store._actions[key] = (store._actions[key] || [])
    store._actions[key].push((payload) => {
      action.call(store, store, payload)
    })
  })

  // 處理getter
  module.forEachGetter((getter, key) => {
    store._wrappedGetters[key] = function() {
      return getter(module.state)
    }
  })

  // 處理children
  module.forEachChild((child, key) => {
    // 遞歸加載
    installModule(store, rootState, path.concat(key), child)
  })

}

此時,已經把每個模塊的actions、mutation、getters都掛到了store上,接下來需要對state處理。

// 將所有的子模塊的狀態(tài)安裝到父模塊的狀態(tài)上
// 需要注意的是vuex 可以動態(tài)的添加模塊
if (path.length > 0) {
  let parent = path.slice(0, -1).reduce((memo, current) => {
    return memo[current]
  }, rootState)
  // 如果這個對象本身不是響應式的 那么Vue.set 就相當于  obj[屬性 ]= 值
  Vue.set(parent, path[path.length - 1], module.state);
}

到此已經完成模塊的安裝,接下里是要把這些放到Vue實例上面

模塊與實例的關聯

constructor(options) {

  const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
  this._mutations = {};
  this._actions = {};
  this._wrappedGetters = {};

  // 1.模塊收集
  this._modules = new ModuleCollection(options);

  // 2.安裝模塊 根模塊的狀態(tài)中 要將子模塊通過模塊名 定義在根模塊上
  installModule(this, state, [], this._modules.root);

  // 3,將狀態(tài)和getters 都定義在當前的vm上
  resetStoreVM(this, state);

}
function resetStoreVM(store, state) {
  const computed = {}; // 定義計算屬性
  store.getters = {}; // 定義store中的getters
  forEachValue(store._wrappedGetters, (fn, key) => {
    computed[key] = () => {
      return fn();
    }
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key] // 去計算屬性中取值
    });
  })
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed // 計算屬性有緩存效果
  });
}

相對應的Store類做以下修改

export class Store {
  constructor(options) {

    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    this._mutations = {};
    this._actions = {};
    this._wrappedGetters = {};

    // 1.模塊收集
    this._modules = new ModuleCollection(options);

    // 2.安裝模塊 根模塊的狀態(tài)中 要將子模塊通過模塊名 定義在根模塊上
    installModule(this, state, [], this._modules.root);

    // 3,將狀態(tài)和getters 都定義在當前的vm上
    resetStoreVM(this, state);

  }

  commit = (type, payload) => { //保證當前this 當前store實例
    this._mutations[type].forEach(mutation => mutation.call(this, payload))
  }

  dispatch = (type, payload) => {
    this._actions[type].forEach(action => action.call(this, payload))
  }

  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

命名空間nameSpaced

默認情況下,模塊內部的 action、mutation 和 getter 是注冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。

如果希望你的模塊具有更高的封裝度和復用性,你可以通過添加 namespaced: true 的方式使其成為帶命名空間的模塊。當模塊被注冊后,它的所有 getter、action 及 mutation 都會自動根據模塊注冊的路徑調整命名。

[圖片上傳失敗...(image-a03e51-1599730267452)]

平常寫上面基本上都要加上 namespaced,防止命名沖突,方法重復多次執(zhí)行。現在就算每個 modules 的方法命一樣,也默認回加上這個方法別包圍的所有父結點的 key,核心就是 path 變量,在安裝模塊的時候把path處理下:

// 我要給當前訂閱的事件 增加一個命名空間
let namespace = store._modules.getNamespaced(path); // 返回前綴即可

store._modules就是模塊收集好的模塊,給它增加一個獲取命名空間的方法。

給ModuleCollection類增加一個getNamespaced方法,其參數就是path。

// 獲取命名空間, 返回一個字符串
getNamespaced(path) {
  let root = this.root; // 從根模塊找起來
  return path.reduce((str, key) => { // [a,c]
    root = root.getChild(key); // 不停的去找當前的模塊
    return str + (root.namespaced ? key + '/' : '')
  }, ''); // 參數就是一個字符串
}

當然Module類也需要增加一個屬性訪問器

get namespaced() {
  return !!this._raw.namespaced
}

接下來就是在處理mutation,action,getters的時候key的值加上namespace就可以了。

// 處理mutation
module.forEachMutation((mutation, key) => {
  store._mutations[namespace + key] = (store._mutations[namespace + key] || [])
  store._mutations[namespace + key].push((payload) => {
    mutation.call(store, module.state, payload)
  })
})

// 處理action
module.forEachAction((action, key) => {
  store._actions[namespace + key] = (store._actions[namespace + key] || [])
  store._actions[namespace + key].push((payload) => {
    action.call(store, store, payload)
  })
})

// 處理getter
module.forEachGetter((getter, key) => {
  store._wrappedGetters[namespace + key] = function() {
    return getter(module.state)
  }
})

namespaces 核心就是對數據格式的處理,來進行發(fā)布與訂閱。

插件

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

使用的時候:

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

在插件中不允許直接修改狀態(tài)——類似于組件,只能通過提交 mutation 來觸發(fā)變化

先來看下一個vuex本地持久化的一個插件

function persists() {
    return function(store) { // store是當前默認傳遞的
        let data = localStorage.getItem('VUEX:STATE');
        if (data) {
            store.replaceState(JSON.parse(data));
        }
        store.subscribe((mutation, state) => {
            localStorage.setItem('VUEX:STATE', JSON.stringify(state));
        })
    }
}

插件返回一個函數,函數的參數就是store。其中replaceState, subscribe是關鍵點,也是vuex其中的2個api,接下來實現一下這2個方法。

export class Store {
  constructor(options) {

    const state = options.state; //數據變化要更新視圖 (vue的核心邏輯依賴收集)
    this._mutations = {};
    this._actions = {};
    this._wrappedGetters = {};

    // 1.模塊收集
    this._modules = new ModuleCollection(options);

    // 2.安裝模塊 根模塊的狀態(tài)中 要將子模塊通過模塊名 定義在根模塊上
    installModule(this, state, [], this._modules.root);

    // 3,將狀態(tài)和getters 都定義在當前的vm上
    resetStoreVM(this, state);

    // 插件內部會依次執(zhí)行
    options.plugins.forEach(plugin=>plugin(this));

  }

  commit = (type, payload) => { //保證當前this 當前store實例
    this._mutations[type].forEach(mutation => mutation.call(this, payload))
  }

  dispatch = (type, payload) => {
    this._actions[type].forEach(action => action.call(this, payload))
  }

  get state() { // 屬性訪問器   new Store().state  Object.defineProperty({get()})
    return this._vm._data.$$state
  }

}

options.plugins.forEach(plugin=>plugin(this))就是讓所有插件依次執(zhí)行,參數就是store.

this._subscribes = [];
// ...
subscribe(fn){
  this._subscribes.push(fn);
}

subscribe就介紹一個函數,放入到一個數組或者隊列中去。

// 處理mutation
module.forEachMutation((mutation, key) => {
  store._mutations[namespace + key] = (store._mutations[namespace + key] || [])
  store._mutations[namespace + key].push((payload) => {
    mutation.call(store, module.state, payload)
    store._subscribes.forEach(fn => {
      fn(mutation, rootState)
    })
  })
})

相應的在安裝模塊處理mutation的時候,需要讓訂閱的store._subscribes執(zhí)行。fn的參數就是mutation和根狀態(tài)。

replaceState(state){
  // 替換掉最新的狀態(tài)
  this._vm._data.$$state = state
}

這是最簡單的改變狀態(tài)的方法,但此時雖然是ok的,但是mutation提交的還是舊值,mutation.call(store, module.state, payload)這個地方還是有點問題,module.state拿到的不是最新的狀態(tài)。

function getState(store, path) { // 獲取最新的狀態(tài) 可以保證視圖更新
  return path.reduce((newState, current) => {
    return newState[current];
  }, store.state);
}

可以通過這個方法能獲取到最新的轉態(tài),相應的在處理mutation,getters的地方做相應調整。

// 處理mutation
module.forEachMutation((mutation, key) => {
  store._mutations[namespace + key] = (store._mutations[namespace + key] || [])
  store._mutations[namespace + key].push((payload) => {
    mutation.call(store, getState(store, path), payload)
    store._subscribes.forEach(fn => {
      fn(mutation, store.state)
    })
  })
})

// 處理getter
module.forEachGetter((getter, key) => {
  store._wrappedGetters[namespace + key] = function() {
    return getter(getState(store, path))
  }
})

之前的mutation.state全部替換成getState去獲取最新的值。n(mutation, rootState) 也替換為fn(mutation, store.state),這樣就可以了。當然源碼中并沒有getState去或獲取最新狀態(tài)的方法。

Vuex中的輔助方法

所謂輔助函數,就是輔助我們平時使用,說白了就是讓我們偷懶。

我們在頁面組件中可能會這樣使用

<template>
  <div id="app">
    我的年齡是:{{this.$store.getters.age}}
    <button @click="$store.commit('changeAge',5)">同步更新age</button>
    <button @click="$store.commit('b/changeAge',10)">異步更新age</button>
  </div>
</template>
<script>

export default {
  computed: {
  },
  mounted() {
    console.log(this.$store);
  },
};
</script>

this.$store.getters.age這樣用當然是可以,但是就是有點啰嗦,我們可以做以下精簡

computed:{
    age() {
    return this.$store.getters.age
  }
}

this.$store.getters.age 直接替換成 age,效果肯定是一樣的。但是寫了在computed中寫了age方法,感覺還是啰嗦麻煩,那再來簡化一下吧,先看下用法:

computed:{
    ...mapState(['age'])
}

mapState實現

export function mapState(stateArr) {
    let obj = {};
    for (let i = 0; i < stateArr.length; i++) {
        let stateName = stateArr[i];
        obj[stateName] = function() {
            return this.$store.state[stateName]
        }
    }
    return obj
}

那如法炮制,mapGetters

export function mapGetters(gettersArr) {
    let obj = {};
    for (let i = 0; i < gettersArr.length; i++) {
        let gettName = gettersArr[i];
        obj[gettName] = function() {
            return this.$store.getters[gettName]
        }
    }
    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;
}

其中這些方法都是在一個helpers文件中。在vuex/index文件中將其導入。

import { Store, install } from './store';

// 這個文件是入口文件,核心就是導出所有寫好的方法
export default {
  Store,
  install
}

export * from './helpers';

createNamespacedHelpers

可以通過使用 createNamespacedHelpers 創(chuàng)建基于某個命名空間輔助函數。它返回一個對象,對象里有新的綁定在給定命名空間值上的組件綁定輔助函數。

export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

總結

vuex的核心功能基本是完成,也能實現基本功能,不過看源碼對很多細節(jié)做了處理,邊界做了判斷。而且其中用到 了很多設計模式以及很多技巧和算法。

通過自己實現一遍vuex,可以加深對vuex的理解和使用。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容