JS手寫狀態管理的實現
一次偶然在掘金看到一位大大分享了老外寫的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); } } } } /** * 釋出主題,觸發訂閱事件 */ pulish() { 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.pulish('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的方法 * @paramkey 的方法屬性名 * @paramnewVal 狀態的新值 */ 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!