Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态只能经过可预测的方式改变。查看官网html
多组件共享同一个状态时,会依赖同一状态 单一数据流没法知足需求:vue
最终致使代码维护困难。es6
multiple components that share a common state: 多组件共享状态 以下图: vuex
Vue.js 官网文档写得很详细,建议初学者必定要把文档至少过一遍。数据库
这里附上文档地址 Vuexapi
解析源码以前,至少要对 Vuex 很是熟悉,再进一步实现。数组
在初始化 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 实例,传入属性对象,可包含如下属性:async
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 对象,属性值有三种形式:
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, 后续的源码分析上会尝试实现。
接下来开始构建一个简易的 vuex
└── 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
}
复制代码
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 属性
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: 以上对源码上有必定出入,简化以后有些属性和方法有所删减和改动
依次执行步骤:
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 是核心,实现状态响应式,一旦发生改变,依赖的组件视图就会当即更新。
类型为 GetterTree 调用 Object.create(null) 建立一个干净的对象,即原型链指向 null,没有原型对象的方法和属性,提升性能
export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
复制代码
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
复制代码
类型为 ActionTree
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
复制代码
考虑到 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>; } 复制代码
core 这里进行了依赖收集,将用户传入的 state 变为响应式数据,数据变化触发依赖的页面更新
get state() {
return this.vm.state;
}
复制代码
订阅的事件在每一次 mutation 时发布
subscribe(fn) {
this.subs.push(fn);
}
复制代码
replaceState(newState) {
this._withCommit(() => {
this.vm.state = newState;
})
}
复制代码
订阅事件的存储队列
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)))
}
复制代码
boolean 监听异步逻辑是否在 dispatch 调用
函数接片,劫持mutation(commit) 触发函数。
_withCommit(fn) {
const committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
复制代码
源码中,在严格模式下,会深度监听状态异步逻辑的调用机制是否符合规范
if (this.strict) {
this.vm.$watch(
() => this.vm.state,
function() {
console.assert(this._committing, '不能异步调用')
},
{
deep: true,
sync: true,
}
);
}
复制代码
Tip: 生产环境下须要禁用 strict 模式,深度监听会消耗性能,核心是调用 Vue 的监听函数
commit = (type, payload) => {
this._withCommit(() => {
this.mutations[type].forEach(fn => fn(payload));
})
}
复制代码
dispatch = (type, payload) => {
this.actions[type].forEach(fn => fn(payload));
}
复制代码
registerModule(moduleName, module) {
this._committing = true;
if (!Array.isArray(moduleName)) {
moduleName = [moduleName];
}
this.modules.register(moduleName, module);
installModule(this, this.state, moduleName, module.rawModule)
}
复制代码
工具方法,注册格式化后的数据,具体表现为: (注册)
/** * * @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 + '/' : '')
}, '');
复制代码
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);
}
复制代码
if (getters) {
foreach(getters, (type, fn) => Object.defineProperty(store.getters, namespace + type, {
get: () => fn(getState(store, path))
}))
}
复制代码
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));
})
})
}
复制代码
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))
}
复制代码
class ModuleCollection{
constructor(options) {
this.register([], options);
}
复制代码
// 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));
}
}
}
复制代码
帮助文件中的四个函数都是经过接受对应要映射为对象的参数名,直接供组件内部使用。
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,其中常规函数,能够在内部访问到当前组件实例
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;
}
复制代码
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;
}
复制代码
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
处理对象键值对迭代函数处理
同步用户调用 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.