What
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态只能通过可预测的方式改变。查看官网
Why
多组件共享同一个状态时,会依赖同一状态 单一数据流无法满足需求:
- 深度嵌套组件级属性传值,会变得非常麻烦
- 同级(兄弟)组件间传值也行不通
- 通过事件或父子组件直接引用也很繁琐
最终导致代码维护困难。
multiple components that share a common state: 多组件共享状态 如下图:
- 可追踪状态改变
- 可维护
- 可进行 time travel
Vue.js 官网文档写得很详细,建议初学者一定要把文档至少过一遍。
这里附上文档地址 Vuex
When
- 当构建一个大型的 SPA 时
- 多组件共享一个状态
Implement
解析源码之前,至少要对 Vuex 非常熟悉,再进一步实现。
新建 store 仓库文件
在初始化 store.js 文件时,需要手动注册 Vuex,这一步是为了将 store 属性注入到每个组件的实例上,可通过 this.$store.state 获取共享状态。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
Tip: 这里 Vue 是在进行订阅,Vuex.install 函数,根组件在实例化时,会自动派发执行 install 方法,并将 Vue 作为参数传入。
导出 Store 实例,传入属性对象,可包含以下属性:
- state: 根状态
- getters: 依赖状态计算出派生状态
- mutations: 通过 commit
- commit 改变状态的唯一方式,使得状态变化可追踪,传入 payload 作为第二参数
- actions: 类似于 mutation,内部执行 commit,异步请求和操作放这里执行,外部触发调用 dispatch
- dispatch 方法,传入 payload 作为第二个参数
- modules: 子模块状态
- namespaced: 字模块设置命名空间
- strict: 严格模式
export default new Vuex.Store({
state: {
todos: [
{ id: 0, done: true, text: 'Vue.js' },
{ id: 1, done: false, text: 'Vuex' },
{ id: 2, done: false, text: 'Vue-router' },
{ id: 3, done: false, text: 'Node.js' },
],
},
getters: {
doneTodosCount(state) {
return state.todos.filter(todo => todo.done).length;
},
},
mutations: {
syncTodoDone(state, id) {
state.todos.forEach((todo, index) => index===id && (todo.done = true))
}
},
actions: {
asyncChange({commit}, payload) {
setTimeout(() => commit('syncChange', payload), 1000);
}
},
modules: {
a: {
state: {
age: '18'
},
mutations: {
syncAgeIncrement(state) {
state.age += 1; // 这里的 state 为当前子模块状态
}
}
}
}
})
store.state.a.age // -> "18"
组件中使用
引入相关映射函数
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';
组件中使用时,这四种映射函数用法差不多,都是返回一个对象。
方式一:
{
computed: {
todos() {
return this.$store.state.todos;
},
age() {
return this.$store.state.a.age
},
// ...
}
}
Tip: 上述方式在获取多个状态时,代码重复过多且麻烦,this.$store.state,可进一步优化为第二种方式
方式二:
{
computed: mapState({
todos: state => state.age,
// or
todos: 'todos', // 属性名为别名
// or
todos(state) {
return state.todos; // 内部可获取 this 获取当前实例数据
}
})
}
可以看到上述的状态映射,调用时可传入 options 对象,属性值有三种形式:
- 箭头函数(简洁)
- 字符串(别名)
- normal 函数(this 指向向前组件实例)
Tip: 如果当前实例组件,有自己的私有的计算属性时,可使用 es6 语法的 Object Spread Operator 对象展开运算符
方式三:
{
computed: {
...mapState([
'todos',
]),
otherValue: 'other value'
}
}
在子模块状态属性中添加 namespaced: true 字段时,mapMutations, mapActions 需要添加对应的命名空间 方式一:
{
methods: {
syncTodoDone(payload) {
this.$store.commit('syncTodoDone', payload)
},
syncAgeIncrement() {
this.$store.commit('syncAgeIncrement')
}
}
}
方式二:
{
methods: {
...mapMutations([
"syncTodoDone",
]),
...mapMutations('a', [
"asyncIncrement"
]),
}
}
方式三: 借助 vuex 内部帮助函数进行包装
import { createNamespacedHelpers } from 'vuex'
const { mapMutations } = createNamespacedHelpers('a')
{
methods: mapMutations([
'syncAgeIncrement'
])
}
除了以上基础用法之外,还有 plugins, registerModule 属性与 api, 后续的源码分析上会尝试实现。
Build
接下来开始构建一个简易的 vuex
Application Structure 目录结构
└── vuex
└── src
├── index.js
├── store.js # core code including install, Store
└── helpers # helper functions including mapState, mapGetters, mapMutations, mapActions, createNamespacedHelpers
└── util.js
├── plugins
│ └── logger.js
└── module
├── module-collection.js
└── module.js
核心入口文件
导出包含核心代码的对象
// index.js
import { Store, install } from './store';
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers';
export default {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions,
createNamespacedHelpers
}
export {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions,
createNamespacedHelpers
}
store.js
实现 install 方法
function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
this.$store = this.$options.store;
} else {
this.$store = this.$parent && this.$parent.$store;
}
},
})
}
Tip: 内部通过调用 Vue.mixin(),为所有组件注入 $store 属性
实现 Store 类
Store 数据结构
interface StoreOptions<S> {
state?: S | (() => S);
getters?: GetterTree<S, S>;
actions?: ActionTree<S, S>;
mutations?: MutationTree<S>;
modules?: ModuleTree<S>;
plugins?: Plugin<S>[];
strict?: boolean;
}
export declare class Store<S> {
constructor(options: StoreOptions<S>);
readonly state: S;
readonly getters: any;
replaceState(state: S): void;
dispatch: Dispatch;
commit: Commit;
subscribe<P extends MutationPayload>(fn: (mutation: P, state: S) => any): () => void;
registerModule<T>(path: string, module: Module<T, S>, options?: ModuleOptions): void;
registerModule<T>(path: string[], module: Module<T, S>, options?: ModuleOptions): void;
}
Tip: 以上对源码上有一定出入,简化之后有些属性和方法有所删减和改动
依次执行步骤:
- 脚本引入时,确保 install 方法被调用,先判断是否挂载了全局属性 Vue
constructor(options = {}) { if (!Vue && typeof Window !== undefined && Window.Vue) { install(Vue); } }
构造函数内部先初始化实例属性和方法
this.strict = options.strict || false;
this._committing = false;
this.vm = new Vue({
data: {
state: options.state,
},
});
this.getters = Object.create(null);
this.mutations = Object.create(null);
this.actions = Object.create(null);
this.subs = [];
Tip: this.vm 是核心,实现状态响应式,一旦发生改变,依赖的组件视图就会立即更新。
getters
类型为 GetterTree 调用 Object.create(null) 创建一个干净的对象,即原型链指向 null,没有原型对象的方法和属性,提高性能
export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
- mutations: MutationTree
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
actions
类型为 ActionTree
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
modules
考虑到 state 对象下可能会有多个 modules,创建 ModuleCollection 格式化成想要的数据结构
export interface Module<S, R> {
namespaced?: boolean;
state?: S | (() => S);
getters?: GetterTree<S, R>;
actions?: ActionTree<S, R>;
mutations?: MutationTree<S>;
modules?: ModuleTree<R>;
}
export interface ModuleTree<R> {
[key: string]: Module<any, R>;
}
get state
core 这里进行了依赖收集,将用户传入的 state 变为响应式数据,数据变化触发依赖的页面更新
get state() {
return this.vm.state;
}
subscribe
订阅的事件在每一次 mutation 时发布
subscribe(fn) {
this.subs.push(fn);
}
- replaceState
replaceState(newState) {
this._withCommit(() => {
this.vm.state = newState;
})
}
subs
订阅事件的存储队列
plugins
const plugins= options.plugins;
plugins.forEach(plugin => plugin(this));
结构为数组,暴露每一次 mutation 的钩子函数。每一个 Vuex 插件仅仅是一个函数,接收 唯一的参数 store,插件功能就是在mutation 调用时增加逻辑,如: - createLogger vuex/dist/logger 修改日志(内置) - stateSnapShot 生成状态快照 - persists 数据持久化
const persists = store => {
// mock data from server db
let local = localStorage.getItem('Vuex:state');
if (local) {
store.replaceState(JSON.parse(local));
}
// mock 每一次数据变了之后就往数据库里存数据
store.subscribe((mutation, state) => localStorage.setItem('Vuex:state', JSON.stringify(state)))
}
_committing
boolean 监听异步逻辑是否在 dispatch 调用
_withCommit
函数接片,劫持mutation(commit) 触发函数。
_withCommit(fn) {
const committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
strict
源码中,在严格模式下,会深度监听状态异步逻辑的调用机制是否符合规范
if (this.strict) {
this.vm.$watch(
() => this.vm.state,
function() {
console.assert(this._committing, '不能异步调用')
},
{
deep: true,
sync: true,
}
);
}
Tip: 生产环境下需要禁用 strict 模式,深度监听会消耗性能,核心是调用 Vue 的监听函数
commit
commit = (type, payload) => {
this._withCommit(() => {
this.mutations[type].forEach(fn => fn(payload));
})
}
dispatch
dispatch = (type, payload) => {
this.actions[type].forEach(fn => fn(payload));
}
registerModule 动态注册状态模块
registerModule(moduleName, module) {
this._committing = true;
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module);
installModule(this, this.state, moduleName, module.rawModule)
}
installModule
工具方法,注册格式化后的数据,具体表现为: (注册)
/**
*
* @param {StoreOption} store 状态实例
* @param {state} rootState 根状态
* @param {Array<String>} path 父子模块名构成的数组
* @param {Object} rawModule 当前模块状态对应格式化后的数据:{ state, _raw, _children, state } 其中 _raw 是 options: { namespaced?, state, getter, mutations, actions, modules?, plugins, strict}
*/
function installModule(store, rootState, path, rawModule) {
let { getters, mutations, actions } = rawModule._raw;
let root = store.modules.root;
- 子模块命名空间处理
const namespace = path.reduce((str, currentModuleName) => {
// root._raw 对应的就是 当前模块的 option, 根模块没有 namespaced 属性跳过
root = root._children[currentModuleName];
return str + (root._raw.namespaced ? currentModuleName + '/' : '')
}, '');
- 注册 state
if (path.length > 0) {
let parentState = path.slice(0, -1).reduce((root, current) => root[current], rootState);
// CORE:动态给跟状态添加新属性,需调用 Vue.set API,添加依赖
Vue.set(parentState, path[path.length - 1], rawModule.state);
}
- 注册 getters
if (getters) {
foreach(getters, (type, fn) => Object.defineProperty(store.getters, namespace + type, {
get: () => fn(getState(store, path))
}))
}
- 注册 mutations
if (mutations) {
foreach(mutations, (type, fn) => {
let arr = store.mutations[namespace + type] || (store.mutations[namespace + type] = []);
arr.push(payload => {
fn(getState(store, path), payload);
// 发布 subscribe 订阅的回调函数
store.subs.forEach(sub => sub({
type: namespace + type,
payload,
}, store.state));
})
})
}
- 注册 actions
if (actions) {
foreach(actions, (type, fn) => {
let arr = store.actions[namespace + type] || (store.actions[namespace + type] = []);
arr.push(payload => fn(store, payload));
})
}
- 递归处理子模块
foreach(rawModule._children, (moduleName, rawModule) => installModule(store, rootState, path.concat(moduleName), rawModule))
}
ModuleCollection 类结构
constructor 构造函数
class ModuleCollection{
constructor(options) {
this.register([], options);
}
register 实例方法
// rootModule: 为当前模块下的 StoreOption
register(path, rootModuleOption) {
let rawModule = {
_raw: rootModuleOption,
_children: Object.create(null),
state: rootModuleOption.state
}
rootModuleOption.rawModule = rawModule;
if (!this.root) {
this.root = rawModule;
} else {
// 若 modules: a.modules.b => [a, b] => root._children.a._children.b
let parentModule = path.slice(0, -1).reduce((root, current) => root._children[current], this.root);
parentModule._children[path[path.length - 1]] = rawModule
}
if (rootModuleOption.modules) {
foreach(rootModuleOption.modules, (moduleName, moduleOption) => this.register(path.concat(moduleName), moduleOption));
}
}
}
helpers.js
帮助文件中的四个函数都是通过接受对应要映射为对象的参数名,直接供组件内部使用。
mapState
export const mapState = (options) => {
let obj = Object.create(null);
if (Array.isArray(options)) {
options.forEach((stateName) => {
obj[stateName] = function() {
return this.$store.state[stateName];
};
});
} else {
Object.entries(options).forEach(([stateName, value]) => {
obj[stateName] = function() {
if (typeof value === "string") {
return this.$store.state[stateName];
}
return value(this.$store.state);
}
});
}
return obj;
};
参数 options 类型可以是: - Array[string], 如:[ 'count', 'list' ] - Object, key 值为状态名,value 可以是 string, arrow function, normal function,其中常规函数,可以在内部访问到当前组件实例
mapGetters
export function mapGetters(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(getterName => {
console.log(getterName)
obj[getterName] = function() {
return this.$store.getters[namespace + getterName];
}
})
return obj;
}
mapMutations
export function mapMutations(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(mutationName => {
obj[mutationName] = function(payload) {
return this.$store.commit(namespace + mutationName, payload)
}
})
return obj;
}
mapActions
export function mapActions(namespace, options) {
let obj = Object.create(null);
if (Array.isArray(namespace)) {
options = namespace;
namespace = '';
} else {
namespace += '/';
}
options.forEach(actionName => {
obj[actionName] = function(payload) {
return this.$store.dispatch(namespace + actionName, payload)
}
})
return obj;
}
以上后三个方法包含了子模块命名空间,参数解析如下:
参数1可选值,为子状态模块的命名空间 参数2为选项属性,类型同 mapState
工具函数
foreach
处理对象键值对迭代函数处理
getState
同步用户调用 replaceState 后,状态内部的新状态
const foreach = (obj, callback) => Object.entries(obj).forEach(([key, value]) => callback(key, value));
const getState = (store, path) => path.reduce((newState, current) => newState[current], store.state);
至此, vuex 源码的个人分析基本完结,因为是简版与源码会有一定的出入。 Feel free to tell me if there is any problem.