一次偶然在掘金看到一位大大分享了老外寫的js狀態管理文章,通讀後就決定自己也實現一遍,目的是瞭解狀態管理的內部機制.
當前的專案多數以元件化開發,狀態管理庫使得元件間狀態管理變得非常方便。
1. 訂閱釋出模組
這個模組實際上是觀察者模式,是一種一對多的依賴關係,當物件的某種狀態發生改變,所有依賴它的物件都將得到通知,觸發已經註冊的事件.
在主題Subject
類中首先定義this.eventList
儲存需要註冊的事件,依次新增subscribe
(訂閱)、unsubscribe
(取消訂閱)、publish
(釋出訂閱)等方法
subscribe
和unsubscribe
的兩個引數: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
,從引數中獲取actions
、mutations
,或者預設為{}
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
中資料通過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
}
複製程式碼
3. 例項化核心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
})
複製程式碼
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. 應用例項
例項將採用兩個元件Index
和Detail
,分別代表兩個頁面,通過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})
}
}
複製程式碼
文章參考原生 JavaScript 實現 state 狀態管理系統
最後感謝原文作者和分享作者!
完整程式碼見Github,歡迎交流和star!