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 和單純的全局對象有以下兩點不同:
- Vuex 的狀態(tài)存儲是響應(yīng)式的从媚。當(dāng) Vue 組件從 store 中讀取狀態(tài)的時候,若 store 中的狀態(tài)發(fā)生變化患整,那么相應(yīng)的組件也會相應(yīng)地得到高效更新静檬。
- 你不能直接改變 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;
通過用法可以知道:
- Vuex是一個對象嘲碧,它作為vue的插件,必然有install方法父阻;
- Vuex中有一個Store類愈涩,在使用的時候有使用new;
- 需要將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 就走到到了訪問器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ù)最終都會嵌套在一棵樹上态鳖。類似于如下
-
模塊內(nèi)部的 action转培、mutation 和 getter 默認(rèn)可是注冊在全局命名空間的,這樣使得多個模塊能夠?qū)ν?mutation 或 action 作出響應(yīng)浆竭。因此在訂閱mutation 和action時必須存儲在數(shù)組中浸须,每次觸發(fā),數(shù)組中的方法都要執(zhí)行邦泄。
Vuex中可以為每個模塊添加namespaced: true來標(biāo)記為當(dāng)前模塊劃分一個命名空間删窒,接下來看下具體怎么實現(xiàn)一個完整的Vuex。
具體實現(xiàn)
總體思路可以分為以下:
- 模塊收集顺囊。就是把用戶傳給store的數(shù)據(jù)進行格式化肌索,格式化成我們想要的結(jié)構(gòu)(樹)
- 安裝模塊。需要將子模塊通過模塊名定義在跟模塊上
- 把狀態(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的理解和使用罐韩。