一次偶然在掘金看到一位大大分享了老外寫的js狀態管理文章,通讀後就決定本身也實現一遍,目的是瞭解狀態管理的內部機制.javascript
當前的項目多數以組件化開發,狀態管理庫使得組件間狀態管理變得很是方便。html
這個模塊其實是觀察者模式,是一種一對多的依賴關係,當對象的某種狀態發生改變,全部依賴它的對象都將獲得通知,觸發已經註冊的事件.vue
在主題Subject
類中首先定義this.eventList
保存須要註冊的事件,依次添加subscribe
(訂閱)、unsubscribe
(取消訂閱)、publish
(發佈訂閱)等方法java
subscribe
和unsubscribe
的兩個參數: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)
}
}
複製代碼
LibStore
類核心LibStore
類須要引入上面的訂閱發佈模塊的主題類,狀態管理我的理解爲一個單例化的主題,全部的狀態事件都在同一個主題下進行訂閱發佈,所以實例化一次Subject
便可。同時須要對state
數據進行監聽和賦值,建立LibStore
類須要傳入參數params
,從參數中獲取actions
、mutations
,或者默認爲{}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
中數據經過commit
或dispatch
方法來執行組件化
/** * 修改狀態值 * @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
}
複製代碼
LibStore
組件使用vuex
的同窗對這個組件必定不陌生,主要是配置state
、mutations
、actions
,並把參數傳入核心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
})
複製代碼
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))
}
}
複製代碼
實例將採用兩個組件Index
和Detail
,分別表明兩個頁面,經過hash
路由切換掛載實現跳轉,須要說明的是,每次掛載組件前須要清除已經在狀態對象的單例化主題中註冊的stateChange
方法,避免重複註冊。
<!-- 頁面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)
}
}
複製代碼
<!-- 頁面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})
}
}
複製代碼
文章參考原生 JavaScript 實現 state 狀態管理系統
最後感謝原文做者和分享做者! 完整代碼見Github,歡迎交流和star!