一次偶然在掘金看到一位大大分享了老外写的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
复制代码首页
<%= count%>
// 组件Jsimport 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
复制代码详情
<%= count%>
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}) }}复制代码
文章参考
最后感谢原文作者和分享作者! 完整代码见,欢迎交流和star!