原生 JavaScript 实现 state 状态管理系统

Build a state management system with vanilla JavaScript | CSS-Tricks
javascript

在软件工程中,状态管理已经不是什么新鲜概念,可是在 JavaScript 语言中比较流行的框架都在使用相关概念。传统意义上,咱们会保持 DOM 自己的状态甚至声明该状态为全局变量。不过如今,咱们有不少状态管理的宠儿供咱们选择。好比 Redux,MobX 以及 Vuex,使得跨组件的状态管理更为方便。这对于一些响应式的框架很是适用,好比 React 或者 Vue。css

然而,这些状态管理库是如何实现的?咱们可否本身创造一个?先不讨论这些,最起码,咱们可以真实地了解状态管理的通用机制和一些流行的 API。html

在开始以前,须要具有 JavaScript 的基础知识。你应该知道数据类型的概念,了解 ES6 相关语法及功能。若是不太了解,去这里学习一下。这篇文章并非要替代 Redux 或者 MobX。在这里咱们进行一次技术探索,各持己见就好。前端

前言

在开始以前,咱们先看看须要达到的效果。java

架构设计

使用你最爱的 IDE,建立一个文件夹:git

~/Documents/Projects/vanilla-js-state-management-boilerplate/
复制代码

项目结构相似以下:github

/src
├── .eslintrc
├── .gitignore
├── LICENSE
└── README.md
复制代码

Pub/Sub

下一步,进入 src 目录,建立 js 目录,下面建立 lib目录,并建立 pubsub.js数组

结构以下:bash

/js
├── lib
└── pubsub.js
复制代码

打开 pubsub.js 由于咱们将要实现一个 订阅/发布 模块。全称 “Publish/Subscribe”。在咱们应用中,咱们会建立一些功能模块用于订阅咱们命名的事件。另外一些模块会发布相应的事件,一般应用在一个相关的负载序列上。架构

Pub/Sub 有时候很难理解,如何去模拟呢?想象一下你工做在一家餐厅,你的用户有一个发射装置和一个菜单。假如你在厨房工做,你知道何时服务员会清除发射装置(下单),而后让大厨知道哪个桌子的发射装置被清除了(下单)。这就是一条对应桌号的点菜线程。在厨房里面,一些厨子须要开始做业。他们是被这条点菜线程订阅了,直到菜品完成,因此厨子知道本身要作什么菜。所以,你手底下的厨师都在为相同的点菜线程(称为 event),去作对应的菜品(称为 callback)。

上图是一个直观的解释。

PubSub 模块会预加载全部的订阅并执行他们各自的回调函数。只须要几行代码就可以建立一个很是优雅地响应流。

pubsub.js 中添加以下代码:

export default class PubSub {
  constructor() {
    this.events = {};
  }
}
复制代码

this.events 用来保存咱们定义的事件。

而后在 constructor 下面增长以下代码:

subscribe(event, callback) {

  let self = this;

  if(!self.events.hasOwnProperty(event)) {
    self.events[event] = [];
  }

  return self.events[event].push(callback);
}
复制代码

这里是一个订阅方法。参数 event 是一个字符串类型, 用于指定惟一的 event 名字用于回调。若是没有匹配的 event 在 events 集合中,那么咱们建立一个空数组用于以后的检查。而后咱们将回调方法 push 到这个 event 集合中。若是存在 event 集合,将回调函数直接 push 进去。最后返回集合长度。

如今咱们须要获取对应的订阅方法,猜猜接下来是什么?大家知道的:是 publish 方法。添加以下代码:

publish(event, data = {}) {

  let self = this;

  if(!self.events.hasOwnProperty(event)) {
    return [];
  }

  return self.events[event].map(callback => callback(data));
} 
复制代码

这个方法首先检查传递的 event 是否存在。若是不存在,返回空数组。若是存在,那么遍历集合中的方法,并将 data 传递进去执行。若是没有回调方法,那也 ok,由于咱们建立的空数组也会适用于 subscribe 方法。

这就是 PubSub。接下来看看是什么!

核心的存储对象 Store

如今咱们已经有了订阅/发布模型,咱们想要建立这个应用的依赖:Store。咱们一点一点来看。

先看一下这个存储对象是用来干什么的。

Store 是咱们的核心对象。每次引入 @import store from '../lib/store.js', 你将会在这个对象中存储你编写的状态位。这个 state 的集合,包含咱们应用的全部状态,它有一个 commit 方法咱们称为 mutations,最后有一个 dispatch 方法咱们称为 actions。在这个核心实现的细节中,应该有一个基于代理(Proxy-based)的系统,用来监听和广播在 PubSub 模型中的状态变化。

咱们建立一个新的文件夹 storejs 下面。而后再建立一个 store.js 的文件。你的 js 目录看起来应该是以下的样子:

/js
└── lib
    └── pubsub.js
└──store
    └── store.js
复制代码

打开 store.js 而且引入 订阅/发布 模块。以下:

import PubSub from '../lib/pubsub.js';
复制代码

这在 ES6 语法中很常见,很是具备辨识性。

下一步,开始建立对象:

export default class Store {
  constructor(params) {
    let self = this;
  }
}
复制代码

这里有一个自我声明。咱们须要建立默认的 stateactions,以及 mutations。咱们也要加入 status 元素用来断定 Store 对象在任意时刻的行为:

self.actions = {};
self.mutations = {};
self.state = {};
self.status = 'resting';
复制代码

在这以后,咱们须要实例化 PubSub,绑定咱们的 Store 做为一个 events 元素:

self.events = new PubSub();
复制代码

接下来咱们须要寻找传递的 params 对象是否包含 actions 或者 mutations。当 Store 初始化时,咱们将数据传递进去。包含一个 actionsmutations 的集合,这个集合用来控制存储的数据:

if(params.hasOwnProperty('actions')) {
  self.actions = params.actions;
}

if(params.hasOwnProperty('mutations')) {
  self.mutations = params.mutations;
}
复制代码

以上是咱们默认设置和可能的参数设置。接下来,让咱们看看 Store 对象如何追踪变化。咱们会用 Proxy 实现。Proxy 在咱们的状态对象中使用了一半的功能。若是咱们使用 get,每次访问数据都会进行监听。一样的选择 set ,咱们的监测将做用于数据改变时。代码以下:

self.state = new Proxy((params.state || {}), {
  set: function(state, key, value) {

    state[key] = value;

    console.log(`stateChange: ${key}: ${value}`);

    self.events.publish('stateChange', self.state);

    if(self.status !== 'mutation') {
      console.warn(`You should use a mutation to set ${key}`);
    }

    self.status = 'resting';

    return true;
  }
});
复制代码

在这个 set 函数中发生了什么?这意味着若是有数据变化如 state.name = 'Foo',这段代码将会运行。及时在咱们的上下文环境中,改变数据并打印。咱们能够发布一个 stateChange 事件到 PubSub 模块。任何订阅的事件的回调函数会执行,咱们检查 Store 的 status,当前的状态应该是 mutation,这意味着状态已经被更新了。咱们能够添加一个警告去提示开发者非 mutation 状态下更新数据的风险。

Dispatch 和 commit

咱们已经将核心的元素添加到 Store 中了,如今咱们添加两个方法。dispatch 用于执行 actionscommit 用于执行 mutations。代码以下:

dispatch (actionKey, payload) {

  let self = this;

  if(typeof self.actions[actionKey] !== 'function') {
    console.error(`Action "${actionKey} doesn't exist.`);
    return false;
  }

  console.groupCollapsed(`ACTION: ${actionKey}`);

  self.status = 'action';

  self.actions[actionKey](self, payload);

  console.groupEnd();

  return true;
}
复制代码

处理过程以下:寻找 action,若是存在,设置 status,而且运行 action。 commit 方法很类似。

commit(mutationKey, payload) {
  let self = this;

  if(typeof self.mutations[mutationKey] !== 'function') {
    console.log(`Mutation "${mutationKey}" doesn't exist`);
    return false;
  }

  self.status = 'mutation';

  let newState = self.mutations[mutationKey](self.state, payload);

  self.state = Object.assign(self.state, newState);

  return true;
}
复制代码

建立一个基础组件

咱们建立一个列表去实践状态管理系统:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/lib/component.js
复制代码
import Store from '../store/store.js';

export default class Component {
  constructor(props = {}) {
    let self = this;

    this.render = this.render || function() {};

    if(props.store instanceof Store) {
      props.store.events.subscribe('stateChange', () => self.render());
    }

    if(props.hasOwnProperty('element')) {
      this.element = props.element;
    }
  }
}
复制代码

咱们看看这一串代码。首先,引入 Store 类。咱们并不想要一个实例,可是更多的检查是放在 constructor 中。在 constructor 中,咱们能够获得一个 render 方法,若是 Component 类是其余类的父类,可能会用到继承类的 render 方法。若是没有对应的方法,那么会建立一个空方法。

以后,咱们检查 Store 类的匹配。须要确认 store 方法是 Store 类的实例,若是不是,则不执行。咱们订阅了一个全局变量 stateChange 事件让咱们的程序得以响应。每次 state 变化都会触发 render 方法。

基于这个基础组件,而后建立其余组件。

建立咱们的组件

建立一个列表:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/component/list.js
复制代码
import Component from '../lib/component.js';
import store from '../store/index.js';

export default class List extends Component {

  constructor() {
    super({
      store,
      element: document.querySelector('.js-items')
    });
  }

  render() {
    let self = this;

    if(store.state.items.length === 0) {
      self.element.innerHTML = `<p class="no-items">You've done nothing yet &#x1f622;</p>`;
      return;
    }

    self.element.innerHTML = ` <ul class="app__items"> ${store.state.items.map(item => { return ` <li>${item}<button aria-label="Delete this item">×</button></li> ` }).join('')} </ul> `;

    self.element.querySelectorAll('button').forEach((button, index) => {
      button.addEventListener('click', () => {
        store.dispatch('clearItem', { index });
      });
    });
  }
};
复制代码

建立一个计数组件:

import Component from '../lib/component.js';
import store from '../store/index.js';

export default class Count extends Component {
  constructor() {
    super({
      store,
      element: document.querySelector('.js-count')
    });
  }

  render() {
    let suffix = store.state.items.length !== 1 ? 's' : '';
    let emoji = store.state.items.length > 0 ? '&#x1f64c;' : '&#x1f622;';

    this.element.innerHTML = ` <small>You've done</small> ${store.state.items.length} <small>thing${suffix} today ${emoji}</small> `;
  }
}
复制代码

建立一个 status 组件:

import Component from '../lib/component.js';
import store from '../store/index.js';

export default class Status extends Component {
  constructor() {
    super({
      store,
      element: document.querySelector('.js-status')
    });
  }

  render() {
    let self = this;
    let suffix = store.state.items.length !== 1 ? 's' : '';

    self.element.innerHTML = `${store.state.items.length} item${suffix}`;
  }
}
复制代码

文件目录结构以下:

/src
├── js
│   ├── components
│   │   ├── count.js
│   │   ├── list.js
│   │   └── status.js
│   ├──lib
│   │  ├──component.js
│   │  └──pubsub.js
└───── store
│      └──store.js
└───── main.js
复制代码

完善状态管理

咱们已经获得前端组件和主要的 Store。如今须要一个初始状态,一些 actionsmutations。在 store 目录下,建立一个新的 state.js 文件:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/store/state.js
复制代码
export default {
  items: [
    'I made this',
    'Another thing'
  ]1
};
复制代码

继续建立 actions.js

export default {
  addItem(context, payload) {
    context.commit('addItem', payload);
  },
  clearItem(context, payload) {
    context.commit('clearItem', payload);
  }
};
复制代码

继续建立 mutation.js

export default {
  addItem(state, payload) {
    state.items.push(payload);

    return state;
  },
  clearItem(state, payload) {
    state.items.splice(payload.index, 1);

    return state;
  }
};
复制代码

最后建立 index.js

import actions from './actions.js';
import mutations from './mutations.js';
import state from './state.js';
import Store from './store.js';

export default new Store({
  actions,
  mutations,
  state
});
复制代码

最后的集成

最后咱们将全部代码集成到 main.js中,还有 index.html 中:

~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/main.js
复制代码
import store from './store/index.js'; 

import Count from './components/count.js';
import List from './components/list.js';
import Status from './components/status.js';

const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');
复制代码

到此一切准备就绪,下面添加交互:

formElement.addEventListener('submit', evt => {
  evt.preventDefault();

  let value = inputElement.value.trim();

  if(value.length) {
    store.dispatch('addItem', value);
    inputElement.value = '';
    inputElement.focus();
  }
});
复制代码

添加渲染:

const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();

countInstance.render();
listInstance.render();
statusInstance.render();
复制代码

至此完成了一个状态管理的系统。

相关文章
相关标签/搜索