原生 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();
複製代碼

至此完成了一個狀態管理的系統。

相關文章
相關標籤/搜索