vuex原理以及實現(xiàn)

vuex官方文檔

Vuex是什么袁稽?

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

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

  1. Vuex 的狀態(tài)存儲是響應(yīng)式的从媚。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時候,若 store 中的狀態(tài)發(fā)生變化患整,那么相應(yīng)的組件也會相應(yīng)地得到高效更新静檬。
  2. 你不能直接改變 store 中的狀態(tài)。改變 store 中的狀態(tài)的唯一途徑就是顯式地提交 (commit) mutation并级。這樣使得我們可以方便地跟蹤每一個狀態(tài)的變化,從而讓我們能夠?qū)崿F(xiàn)一些工具幫助我們更好地了解我們的應(yīng)用侮腹。

實現(xiàn)簡易版的vuex

先來看下vuex的基本用法

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

Vue.use(Vuex); // 使用這個插件  內(nèi)部會調(diào)用Vuex中的install方法

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

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

store文件

vuex/store.js

export let Vue;

export class Store {
  
}

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

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

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

export class Store {
  
}

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

vuex/mixin.js

export default function applyMixin(Vue) {
  // 父子組件的beforecreate執(zhí)行順序
  Vue.mixin({ // 內(nèi)部會把生命周期函數(shù) 拍平成一個數(shù)組 
    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實現(xiàn)

export class Store {
  constructor(options) {
    const state = options.state; //數(shù)據(jù)變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認(rèn)不會將這個屬性掛載到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還不是響應(yīng)式鸠窗,可以借助new Vue中data的數(shù)據(jù)是響應(yīng)式處理這個問題,將state掛載到$$state上胯究,這個屬性是不會被vue暴露出去(可能是內(nèi)部做了處理)稍计。當(dāng)我們在組件中去獲取值的時候,比如this.store.state.age時候 this.store.state 就走到到了訪問器get state() 就會將整個倉庫的state返回出去裕循,而且數(shù)據(jù)是響應(yīng)式的臣嚣。至于為什么在_vm._data上,需要去看下vue源碼實現(xiàn)剥哑。

getters實現(xiàn)

export class Store {
  constructor(options) {
    // 1.處理state
    const state = options.state; //數(shù)據(jù)變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認(rèn)不會將這個屬性掛載到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; //數(shù)據(jù)變化要更新視圖 (vue的核心邏輯依賴收集)
    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認(rèn)不會將這個屬性掛載到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; //數(shù)據(jù)變化要更新視圖 (vue的核心邏輯依賴收集)
    const computed = {};

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

    this._vm = new Vue({
      data: { // 屬性如果是通過$開頭的 默認(rèn)不會將這個屬性掛載到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) 和 一個 回調(diào)函數(shù) (handler)

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

  • 通過commit方法觸發(fā)調(diào)用士败。

mutation實現(xiàn)

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

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

commit使用箭頭函數(shù)是為了保證調(diào)用的都是當(dāng)前實例,一是通過this.commit(type,data),二是在action中被解構(gòu)使用changeAge({commit},payload){}

actions和dispath也是如此谅将。

完整的Store類

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

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

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

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

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

  }

  commit = (type, payload) => { //保證當(dāng)前this 當(dāng)前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; //數(shù)據(jù)變化要更新視圖 (vue的核心邏輯依賴收集)
    const computed = {};

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

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

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

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

  }

  commit = (type, payload) => { //保證當(dāng)前this 當(dāng)前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的構(gòu)造函數(shù)
export const install = (_vue) => {
  // 需要保存Vue,用戶傳入的Vue構(gòu)造函數(shù)
  Vue = _vue;
  // 需要將根組件中注入的store 分派給每一個組件 (子組件) Vue.mixin
  applyMixin(Vue);
}

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

完整版Vuex實現(xiàn)

我們實現(xiàn)了一個簡易版的Vuex稽煤,對state,actions,mutations,getters 進行了功能的實現(xiàn)。但是沒有對modules進行處理猛们,其實modules才是Vuex中最核心并且是最難實現(xiàn)的念脯。

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

  • state: 所有模塊中的state中數(shù)據(jù)最終都會嵌套在一棵樹上态鳖。類似于如下
image
  • 模塊內(nèi)部的 action转培、mutation 和 getter 默認(rèn)可是注冊在全局命名空間的,這樣使得多個模塊能夠?qū)ν?mutation 或 action 作出響應(yīng)浆竭。因此在訂閱mutation 和action時必須存儲在數(shù)組中浸须,每次觸發(fā),數(shù)組中的方法都要執(zhí)行邦泄。

    image

Vuex中可以為每個模塊添加namespaced: true來標(biāo)記為當(dāng)前模塊劃分一個命名空間删窒,接下來看下具體怎么實現(xiàn)一個完整的Vuex。

具體實現(xiàn)

總體思路可以分為以下:

  1. 模塊收集顺囊。就是把用戶傳給store的數(shù)據(jù)進行格式化肌索,格式化成我們想要的結(jié)構(gòu)(樹)
  2. 安裝模塊。需要將子模塊通過模塊名定義在跟模塊上
  3. 把狀態(tài)state和getters定義到當(dāng)前的vm上特碳。

模塊收集

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

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

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

  }
}

ModuleCollection 類的實現(xiàn)

這個類是收集模塊诚亚,格式化數(shù)據(jù)用的,那我們先要知道需要什么樣的格式午乓。

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

最終需要的是這樣一個數(shù)結(jié)構(gòu)站宗。

export default class ModuleCollection {
  constructor(options) {
    // 注冊模塊 需要用到棧結(jié)構(gòu)數(shù)據(jù),[根益愈,a],每次循環(huán)遞歸的時候?qū)⑵淙霔7萜埂_@樣每個模塊可以清楚的知道自己的父級是誰
    this.register([], options)
  }

  
  register(path, rootModule) {
    // 格式化后的結(jié)果
    let newModule = { 
      _raw: rootModule, // 用戶定義的模塊
      _children: {}, // 模塊的兒子
      state: {} // 當(dāng)前模塊的狀態(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是空數(shù)組,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但惶。這樣寫會把有多層路徑的模塊最后一項也提到和它平級耳鸯,因此需要確定這個模塊的父級是誰,再把當(dāng)前模塊掛到父級就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收集后需要對收集到數(shù)據(jù)進行整理

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

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

this就是store, 需要完成installModule方法财喳。installModule中傳入的有當(dāng)前模塊,這個模塊可能有自己的方法斩狱。為此先改造下代碼耳高,創(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中相應(yīng)的地方稍作修改所踊。

import Module from './module'

export default class ModuleCollection {
  constructor(options) {
    // 注冊模塊 需要用到棧結(jié)構(gòu)數(shù)據(jù)泌枪,[根,a],每次循環(huán)遞歸的時候?qū)⑵淙霔o醯骸_@樣每個模塊可以清楚的知道自己的父級是誰
    this.register([], options)
  }

  register(path, rootModule) {
    // 格式化后的結(jié)果
    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) {
  // 這里我需要遍歷當(dāng)前模塊上的 actions、mutation继薛、getters 都把他定義在store的_actions, _mutations, _wrappedGetters 中
  
}

installModule 就需要循環(huán)對當(dāng)前模塊處理對應(yīng)的actions修壕、mutation、getters惋增。為此可以對Module類增加方法叠殷,來讓其內(nèi)部自己處理。

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) {
  // 這里我需要遍歷當(dāng)前模塊上的 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)
  })

}

此時,已經(jīng)把每個模塊的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)
  // 如果這個對象本身不是響應(yīng)式的 那么Vue.set 就相當(dāng)于  obj[屬性 ]= 值
  Vue.set(parent, path[path.length - 1], module.state);
}

到此已經(jīng)完成模塊的安裝胖腾,接下里是要把這些放到Vue實例上面

模塊與實例的關(guān)聯(lián)

constructor(options) {

  const state = options.state; //數(shù)據(jù)變化要更新視圖 (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 都定義在當(dāng)前的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 // 計算屬性有緩存效果
  });
}

相對應(yīng)的Store類做以下修改

export class Store {
  constructor(options) {

    const state = options.state; //數(shù)據(jù)變化要更新視圖 (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 都定義在當(dāng)前的vm上
    resetStoreVM(this, state);

  }

  commit = (type, payload) => { //保證當(dāng)前this 當(dāng)前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

默認(rèn)情況下,模塊內(nèi)部的 action、mutation 和 getter 是注冊在全局命名空間的——這樣使得多個模塊能夠?qū)ν?mutation 或 action 作出響應(yīng)咸作。

如果希望你的模塊具有更高的封裝度和復(fù)用性陨晶,你可以通過添加 namespaced: true 的方式使其成為帶命名空間的模塊。當(dāng)模塊被注冊后肯适,它的所有 getter望迎、action 及 mutation 都會自動根據(jù)模塊注冊的路徑調(diào)整命名。

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

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

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

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

給ModuleCollection類增加一個getNamespaced方法,其參數(shù)就是path缺脉。

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

當(dāng)然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 核心就是對數(shù)據(jù)格式的處理,來進行發(fā)布與訂閱攻礼。

插件

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

使用的時候:

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

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

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

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

插件返回一個函數(shù),函數(shù)的參數(shù)就是store太伊。其中replaceState, subscribe是關(guān)鍵點雇锡,也是vuex其中的2個api,接下來實現(xiàn)一下這2個方法。

export class Store {
  constructor(options) {

    const state = options.state; //數(shù)據(jù)變化要更新視圖 (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 都定義在當(dāng)前的vm上
    resetStoreVM(this, state);

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

  }

  commit = (type, payload) => { //保證當(dāng)前this 當(dāng)前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í)行,參數(shù)就是store.

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

subscribe就介紹一個函數(shù)僚焦,放入到一個數(shù)組或者隊列中去锰提。

// 處理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)
    })
  })
})

相應(yīng)的在安裝模塊處理mutation的時候,需要讓訂閱的store._subscribes執(zhí)行芳悲。fn的參數(shù)就是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);
}

可以通過這個方法能獲取到最新的轉(zhuǎn)態(tài)融蹂,相應(yīng)的在處理mutation,getters的地方做相應(yīng)調(diào)整旺订。

// 處理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)超燃,這樣就可以了区拳。當(dāng)然源碼中并沒有g(shù)etState去或獲取最新狀態(tài)的方法。

Vuex中的輔助方法

所謂輔助函數(shù)淋纲,就是輔助我們平時使用劳闹,說白了就是讓我們偷懶。

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

<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這樣用當(dāng)然是可以洽瞬,但是就是有點啰嗦,我們可以做以下精簡

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

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

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

mapState實現(xiàn)

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文件中將其導(dǎo)入放祟。

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

// 這個文件是入口文件鳍怨,核心就是導(dǎo)出所有寫好的方法
export default {
  Store,
  install
}

export * from './helpers';

createNamespacedHelpers

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

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

總結(jié)

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

通過自己實現(xiàn)一遍vuex,可以加深對vuex的理解和使用罐韩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市污朽,隨后出現(xiàn)的幾起案子散吵,更是在濱河造成了極大的恐慌,老刑警劉巖蟆肆,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矾睦,死亡現(xiàn)場離奇詭異,居然都是意外死亡颓芭,警方通過查閱死者的電腦和手機顷锰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亡问,“玉大人官紫,你說我怎么就攤上這事肛宋。” “怎么了束世?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵酝陈,是天一觀的道長。 經(jīng)常有香客問我毁涉,道長沉帮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任贫堰,我火速辦了婚禮穆壕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘其屏。我一直安慰自己喇勋,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布偎行。 她就那樣靜靜地躺著川背,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛤袒。 梳的紋絲不亂的頭發(fā)上熄云,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音妙真,去河邊找鬼缴允。 笑死,一個胖子當(dāng)著我的面吹牛隐孽,可吹牛的內(nèi)容都是我干的癌椿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼菱阵,長吁一口氣:“原來是場噩夢啊……” “哼踢俄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起晴及,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤都办,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后虑稼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琳钉,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年蛛倦,在試婚紗的時候發(fā)現(xiàn)自己被綠了歌懒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡溯壶,死狀恐怖及皂,靈堂內(nèi)的尸體忽然破棺而出甫男,到底是詐尸還是另有隱情,我是刑警寧澤验烧,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布板驳,位于F島的核電站,受9級特大地震影響碍拆,放射性物質(zhì)發(fā)生泄漏若治。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一感混、第九天 我趴在偏房一處隱蔽的房頂上張望端幼。 院中可真熱鬧,春花似錦弧满、人聲如沸静暂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摹迷,卻和暖如春疟赊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背峡碉。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工近哟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鲫寄。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓吉执,卻偏偏與公主長得像,于是被迫代替她去往敵國和親地来。 傳聞我的和親對象是個殘疾皇子戳玫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345