vuex原理以及實現

vuex官方文檔vue

Vuex是什麼?

Vuex 是一個專爲 Vue.js 應用程序開發的 狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化

每個 Vuex 應用的核心就是 store(倉庫)。「store」基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)。Vuex 和單純的全局對象有如下兩點不一樣:git

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

實現簡易版的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的參數是狀態  
 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

vuex/index.js設計模式

import { Store, install } from './store';   
// 這個文件是入口文件,核心就是導出全部寫好的方法  
export default {  
 Store, install  
}

store文件

vuex/store.jsapi

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執行順序  
 Vue.mixin({ // 內部會把生命週期函數 拍平成一個數組   
beforeCreate: vuexInit  
 });}  
  
// 組件渲染時從父=》子  
  
function vuexInit() {  
 // 給全部的組件增長$store 屬性 指向咱們建立的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,actionsapp

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  
 }  
}

經過循環用戶傳進來的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);  
 // 當取值的時候執行計算屬性的邏輯,此時就有緩存功能  
 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])執行計算屬性的邏輯。vuex的做者真是腦洞大開,鬼才啊,這都能想到。

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

  • 對傳入的屬性進行遍歷訂閱
  • 經過commit方法觸發調用。

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);  
 // 當取值的時候執行計算屬性的邏輯,此時就有緩存功能  
 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);  
 // 當取值的時候執行計算屬性的邏輯,此時就有緩存功能  
 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(模塊),在模塊中還能夠繼續嵌套子模塊。
  • state: 全部模塊中的state中數據最終都會嵌套在一棵樹上。相似於以下

  • 模塊內部的 action、mutation 和 getter 默承認是註冊在全局命名空間的,這樣使得多個模塊可以對同一 mutation 或 action 做出響應。所以在訂閱mutation 和action時必須存儲在數組中,每次觸發,數組中的方法都要執行。

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

具體實現

整體思路能夠分爲如下:

  1. 模塊收集。就是把用戶傳給store的數據進行格式化,格式化成咱們想要的結構(樹)
  2. 安裝模塊。須要將子模塊經過模塊名定義在跟模塊上
  3. 把狀態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的狀態'    
        },  
 b:{  
 _raw:"b模塊",  
 _children:{},  
 state:'b的狀態'    
        }  
 }, state:'根模塊本身的狀態'  
 }```  
  
最終須要的是這樣一個數結構。

export default class ModuleCollection {
constructor(options) {
// 註冊模塊 須要用到棧結構數據,[根,a],每次循環遞歸的時候將其入棧。這樣每一個模塊能夠清楚的知道本身的父級是誰
this.register([], options)
}
register(path, rootModule) {
// 格式化後的結果
let newModule = {_raw: rootModule, // 用戶定義的模塊
_children: {}, // 模塊的兒子
state: {} // 當前模塊的狀態
}

if (path.length === 0) { // 說明是根模塊

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

第一次進來的時候path是空數組,root就是用戶傳進去的模塊對象;若是模塊有modules屬性,須要循環去註冊這個模塊。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.安裝模塊 根模塊的狀態中 要將子模塊經過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

this就是store, 須要完成installModule方法。installModule中傳入的有當前模塊,這個模塊可能有本身的方法。爲此先改造下代碼,建立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
}
// 給模塊繼續擴展方法
}

export default Module;

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

import Module from './module'

export default class ModuleCollection {
constructor(options) {
// 註冊模塊 須要用到棧結構數據,[根,a],每次循環遞歸的時候將其入棧。這樣每一個模塊能夠清楚的知道本身的父級是誰
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) {
// 循環模塊 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 就須要循環對當前模塊處理對應的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
}
// 給模塊繼續擴展方法
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處理。

// 將全部的子模塊的狀態安裝到父模塊的狀態上
// 須要注意的是vuex 能夠動態的添加模塊
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.安裝模塊 根模塊的狀態中 要將子模塊經過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

// 3,將狀態和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.安裝模塊 根模塊的狀態中 要將子模塊經過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

// 3,將狀態和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 都會自動根據模塊註冊的路徑調整命名。  
  
![](https://gitee.com/yutao618/images/raw/master/images/170ade78c9e40e00.png)  
  
日常寫上面基本上都要加上 namespaced,防止命名衝突,方法重複屢次執行。如今就算每一個 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 核心就是對數據格式的處理,來進行發佈與訂閱。  
  
### 插件  
  
> Vuex 的 store 接受 `plugins` 選項,這個選項暴露出每次 mutation 的鉤子。Vuex 插件就是一個函數,它接收 store 做爲惟一參數  
  
使用的時候:

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

在插件中不容許直接修改狀態——相似於組件,只能經過提交 mutation 來觸發變化  
  
先來看下一個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.安裝模塊 根模塊的狀態中 要將子模塊經過模塊名 定義在根模塊上
installModule(this, state, [], this._modules.root);

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

// 插件內部會依次執行
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))就是讓全部插件依次執行,參數就是**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執行。fn的參數就是mutation和根狀態。

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

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

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

能夠經過這個方法能獲取到最新的轉態,相應的在處理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去或獲取最新狀態的方法。  
  
### 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` 建立基於某個命名空間輔助函數。它返回一個對象,對象裏有新的綁定在給定命名空間值上的組件綁定輔助函數。

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的核心功能基本是完成,也能實現基本功能,不過看源碼對不少細節作了處理,邊界作了判斷。並且其中用到 了不少設計模式以及不少技巧和算法。
相關文章
相關標籤/搜索