JS手寫狀態管理的實現

靖風行發表於2019-03-03

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

當前的專案多數以元件化開發,狀態管理庫使得元件間狀態管理變得非常方便。

1. 訂閱釋出模組

這個模組實際上是觀察者模式,是一種一對多的依賴關係,當物件的某種狀態發生改變,所有依賴它的物件都將得到通知,觸發已經註冊的事件.

在主題Subject類中首先定義this.eventList儲存需要註冊的事件,依次新增subscribe(訂閱)、unsubscribe(取消訂閱)、publish(釋出訂閱)等方法

subscribeunsubscribe的兩個引數:name代表註冊事件的唯一名字,fn為事件name的回撥函式,表示所有fn方法都註冊到名為name的集合下

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)
    }
  }
}
複製程式碼

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

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,或者預設為{}

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

為了判LibStore物件在任意時刻的狀態,需要定義status用來記錄,狀態有三種:

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!

相關文章