JS手寫狀態管理的實現

一次偶然在掘金看到一位大大分享了老外寫的js狀態管理文章,通讀後就決定本身也實現一遍,目的是瞭解狀態管理的內部機制.javascript

當前的項目多數以組件化開發,狀態管理庫使得組件間狀態管理變得很是方便。html

1. 訂閱發佈模塊

這個模塊其實是觀察者模式,是一種一對多的依賴關係,當對象的某種狀態發生改變,全部依賴它的對象都將獲得通知,觸發已經註冊的事件.vue

在主題Subject類中首先定義this.eventList保存須要註冊的事件,依次添加subscribe(訂閱)、unsubscribe(取消訂閱)、publish(發佈訂閱)等方法java

subscribeunsubscribe的兩個參數:name表明註冊事件的惟一名字,fn爲事件name的回調函數,表示全部fn方法都註冊到名爲name的集合下git

class Subject {
  constructor() {
    this.eventList = []
  }
  /** * 訂閱主題 * @param {string} name 事件名稱 * @param {function} fn 事件方法 */
  subscribe(name, fn) {
    if (!this.eventList.hasOwnProperty(name)) {
      this.eventList[name] = []
    }
    this.eventList[name].push(fn)
    console.log('this.eventList: ', this.eventList);
  }
  /** * 取消訂閱主題 * @param {string} name 事件名稱 * @param {function} fn 事件方法 */
  unsubscribe(name, fn) {
    var fns = this.eventList[name];
    if (!fns || fns.length == 0) { // 若是沒有訂閱該事件,直接返回
      return false
    }
    if (!fn) { // 若是傳入具體函數,表示取消全部對應name的訂閱
      fns.length = 0
    } else {
      for (var i = 0; i < fns.length; i++) {
        if (fn == fns[i]) {
          fns.splice(i, 1);
        }
      }
    }
  }
  /** * 發佈主題,觸發訂閱事件 */
  publish() {
    var name = Array.prototype.shift.call(arguments)	// 獲取事件名稱
    var fns = this.eventList[name]
    if (!fns || fns.length == 0) { // 沒有訂閱該事件
      return false
    }
    for (var i = 0, fn; i < fns.length; i++) {
      fn = fns[i]
      fn.apply(this, arguments)
    }
  }
}
複製代碼

對於觀察者類,傳入主題、事件名稱、事件方法,目的是將事件註冊到相應主題上:github

class Observer {
  constructor(subject, name, fn) {
    this.subject = subject
    this.name = name
    this.subject.subscribe(name, fn)
  }
}
複製代碼

2. 核心LibStore

核心LibStore類須要引入上面的訂閱發佈模塊的主題類,狀態管理我的理解爲一個單例化的主題,全部的狀態事件都在同一個主題下進行訂閱發佈,所以實例化一次Subject便可。同時須要對state數據進行監聽和賦值,建立LibStore類須要傳入參數params,從參數中獲取actionsmutations,或者默認爲{}vuex

constructor(params){
  var _self = this
  this._subject = new Subject()
  this.mutations = params.mutations ? params.mutations : {}
  this.actions = params.actions ? params.actions : {}
}
複製代碼

爲了判LibStore對象在任意時刻的狀態,須要定義status用來記錄,狀態有三種:app

this.status = 'resting';
this.status = 'mutation'; 
this.status = 'action';
複製代碼

存放數據state也會從params傳入,但爲了監聽LibStore中存儲的數據變化,咱們引入了代理Proxy,使每次訪問和改變state數據變化都獲得監聽,改變state數據時觸發主題發佈,執行全部依賴stateChange事件的方法。函數

// 代理狀態值,監聽狀態變化
this.state = new Proxy(params.state || {}, {
  get(state, key) {
    return state[key]
  },
  set(state, key, val) {
    if (_self.status !== 'mutation') {
      console.warn(`須要採用mutation來改變狀態值`);
    }
    state[key] = val
    console.log(`狀態變化:${key}:${val}`)
    _self._subject.publish('stateChange', _self.state)
    _self.status = 'resting';
    return true
  }
})
複製代碼

改變state中數據經過commitdispatch方法來執行組件化

/** * 修改狀態值 * @param {string} name * @param {string} newVal */
commit(name, newVal) {
  if (typeof (this.mutations[name]) != 'function') {
    return fasle
  }
  console.group(`mutation: ${name}`);
  this.status = 'mutation'; // 改變狀態
  this.mutations[name](this.state, newVal);
  console.groupEnd();
  return true;
}
/** * 分發執行action的方法 * @param key 的方法屬性名 * @param newVal 狀態的新值 */
dispatch(key, newVal) {
  if (typeof (this.actions[key]) != 'function') {
    return fasle
  }
  console.group(`action: ${key}`);
  this.actions[key](this, newVal);
  self.status = 'action';
  console.groupEnd();
  return true
}
複製代碼

最後,將實例化的主題_subject暴露出來,以便後續註冊stateChange事件時使用

getSubject() {
   return this._subject
 }
複製代碼

3. 實例化核心LibStore組件

使用vuex的同窗對這個組件必定不陌生,主要是配置statemutationsactions,並把參數傳入核心LibStore組件類的實例當中

import libStore from "./libStore";
let state = {
  count: 0
}
let mutations = {
  addCount(state, val) {
    state.count = val
  },
}
let actions = {
  updateCount(context, val) {
    context.commit('addCount', val);
  }
}
export default new libStore({
  state,
  mutations,
  actions
})
複製代碼

4.註冊stateChange事件

StoreChange類將做爲應用組件的繼承類使用,目的是使使用組件註冊stateChange事件,同時得到繼承類的update方法,該方法將在state數據變化時的到觸發。

引入剛剛實例化LibStore的對象store和訂閱發佈模塊中的觀察者類,並註冊stateChange事件和回調update方法

import store from '@/assets/lib/store'
import { Observer } from './subject'
class StoreChange {
  constructor() {
    this.update = this.update || function () {};
    new Observer(store.getSubject(), 'stateChange', this.update.bind(this))
  }
}
複製代碼

5. 應用實例

實例將採用兩個組件IndexDetail,分別表明兩個頁面,經過hash路由切換掛載實現跳轉,須要說明的是,每次掛載組件前須要清除已經在狀態對象的單例化主題中註冊的stateChange方法,避免重複註冊。

  • Index
<!-- 頁面art模板 -->
<div class="index">
  <h1>首頁</h1>
  <hr>
  <button id="btn1">增長數量</button>
  <button id="btn2">減小數量</button>
  <h3 id='time'><%= count%></h3>
</div>
複製代碼
// 組件Js
import StateChange from '@/assets/lib/stateChange'
import store from '@/assets/lib/store'
export default class Index extends StateChange{
  constructor($root){
    super()
    this.$root = $root
    this.render()
    document.querySelector('#btn1').addEventListener('click',this.add.bind(this))
    document.querySelector('#btn2').addEventListener('click',this.minus.bind(this))
  }
  render(){
    var indexTmpl = require('./index.art')
    this.$root.innerHTML =indexTmpl({count:store.state.count})
  }
  update(){
    document.querySelector('#time').textContent = store.state.count
  }
  add(){
    var count = store.state.count
    store.commit('addCount',++count)
  }
  minus(){
    var count = store.state.count
    store.dispatch('updateCount',--count)
  }
}
複製代碼
  • Detail
<!-- 頁面art模板 -->
<div class="detail">
  <h1>詳情</h1>
  <hr>
  <h3 id="count"><%= count%></h3>
</div>
複製代碼
import StateChange from '@/assets/lib/stateChange'
import store from '@/assets/lib/store'
export default class Index extends StateChange {
  constructor($root){
    super()
    this.$root = $root
    this.render()
  }
  render(){
    var detailTmpl = require('./detail.art')
    this.$root.innerHTML = detailTmpl({count:store.state.count})
  }
}
複製代碼

Demo預覽

文章參考原生 JavaScript 實現 state 狀態管理系統

最後感謝原文做者和分享做者! 完整代碼見Github,歡迎交流和star!

相關文章
相關標籤/搜索