「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
创建属于自己的状态管理
习惯上,我们会直接将状态保持在 DOM 上,甚至将其分配给 window 中的全局对象。但是现在,我们已经有了许多选择,库和框架可以帮助我们管理状态。像 Redux,MobX 和 Vuex 这样的库可以轻松管理跨组件状态。它大大提升了应用程序的扩展性,并且它对于状态优先的响应式框架(如 React 或 Vue)非常有用。
我们自己写个状态管理,应付一个小型应用。 因为文章的主要精力在于状态管理,对于 HTML + CSS 部分,就不做过多的解说了。
/src
├── .eslintrc
├── .gitignore
├── LICENSE
└── README.md
在 js 文件夹。创建 lib 文件夹。在里面,创建一个名为 pubsub.js 的新文件。
/js
├── lib
└── pubsub.js
看着这个名字,大概就可以猜到我们下一步要干嘛了,对,发布/订阅
举一个例子: 玛卡巴卡在网上看上一个小推车,但联系到卖家后,发现小推车卖光了,但是玛卡巴卡对这个小推c车又非常喜欢,所以就联系卖家,问卖家什么时候有货,卖家告诉她,要等一个星期后才有货,卖家告诉玛卡巴卡,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以玛卡巴卡收藏了此店铺。唔西迪西和依古比古看了图片之后,也喜欢这个小推车,也收藏了该店铺;等来货的时候就依次会通知他们。你看这是一个典型的发布订阅模式,卖家是属于发布者,玛卡巴卡和唔西迪西等属于订阅者,订阅该店铺,卖家作为发布者,当小推车到了的时候,会依次通知玛卡巴卡,唔西迪西等,使用旺旺等工具给他们发布消息。
PubSub 模式遍历所有订阅,并触发其回调,同时传入相关的载荷。为程序创建一个非常优雅的响应式流程的好方法。
pubsub.js:
export default class PubSub {
constructor() {
this.events = {};
}
}
this.events 对象将保存我们的具名事件。
subscribe(event, callback) {
let self = this
if (!self.events.hasOwnProperty(event)) {
self.events[event] = []
}
return self.events[event].push(callback)
}
events 中没有匹配的事件,使用空数组创建它。然后,将回调添加到该集合中。如果它已经存在,就直接将回调添加到该集合中。
publish(event, data = {}) {
let self = this
if (!self.events.hasOwnProperty(event)) {
return []
}
return self.events[event].map(callback => callback(data))
}
如果有事件,我们遍历每个存储的回调并将数据传递给它。
Store 对象(核心)
Store 是我们的核心对象。它将包含一个 state 对象,该对象又包含应用程序状态,一个 commit 方法,它将调用 mutations,一个 dispatch 函数将调用 actions。
/js
└── lib
└── pubsub.js
└──store
└── store.js
添加 state,actions 和 mutations 添加默认对象。status 属性,用它来确定对象在任意给定时间正在做什么。PubSub 实例,它将作为 store 的 events 属性的值。actions和mutation ,将控制着我们 store 中的数据流。
import PubSub from '../lib/pubsub.js';
export default class Store {
constructor(params) {
let self = this;
self.actions = {};
self.mutations = {};
self.state = {};
self.status = 'resting';
self.events = new PubSub();
if (params.hasOwnProperty('actions')) {
self.actions = params.actions;
}
if (params.hasOwnProperty('mutations')) {
self.mutations = params.mutations;
}
}
}
在应用和 Store 对象的核心之间,将有一个基于代理的系统,它将使用我们的 PubSub 模块监视和广播状态变化。Proxy(代理)所做的工作主要是代理 state 对象。如果我们添加一个 get 拦截方法,我们可以在每次询问对象数据时进行监控。与 set 拦截方法类似,我们可以密切关注对象所做的更改。
self.state = new Proxy((params.state || {}), {
set: function (state, key, value) {
state[key] = value;
console.log(`stateChange: ${key}: ${value}`);
self.events.publish('stateChange', self.state);
if (self.status !== 'mutation') {
console.warn(`You should use a mutation to set ${key}`);
}
self.status = 'resting';
return true;
}
});
当 mutation 运行类似于 state.name ='Foo' 时,这个拦截器会在它被设置之前捕获它,用 PubSub 模块发布一个 stateChange 事件。任何订阅了该事件的回调将被调用。最后,检查 Store 的状态。如果它当前不是一个 mutation,则可能意味着状态是手动更新的。
添加两个方法。一个是将调用我们 actions 的 dispatch,另一个是将调用我们 mutation 的 commit。
dispatch(actionKey, payload) {
let self = this;
if (typeof self.actions[actionKey] !== 'function') {
console.error(`Action "${actionKey} doesn't exist.`);
return false;
}
console.groupCollapsed(`ACTION: ${actionKey}`);
self.status = 'action';
self.actions[actionKey](self, payload);
console.groupEnd();
return true;
}
commit(mutationKey, payload) {
let self = this;
if (typeof self.mutations[mutationKey] !== 'function') {
console.log(`Mutation "${mutationKey}" doesn't exist`);
return false;
}
self.status = 'mutation';
let newState = self.mutations[mutationKey](self.state, payload);
self.state = Object.assign(self.state, newState);
return true;
}
创建基础组件
/js
├── lib
└── component.js
订阅了全局 stateChange 事件,所以我们的对象可以做到响应式。每次状态改变时都会调用 render 函数。
import Store from '../store/store.js';
export default class Component {
constructor(props = {}) {
let self = this;
this.render = this.render || function () { };
if (props.store instanceof Store) {
props.store.events.subscribe('stateChange', () => self.render());
}
if (props.hasOwnProperty('element')) {
this.element = props.element;
}
}
}
创建我们的组件
/js
├── components
└── count.js
└── list.js
└── status.js
list.js
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class List extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-items')
});
}
render() {
if (store.state.items.length === 0) {
this.element.innerHTML = `<p class="no-items">You've done nothing yet 😢</p>`;
return;
}
this.element.innerHTML = `
<ul class="app__items">
${store.state.items.map(item => {
return `
<li>${item}<button aria-label="Delete this item">×</button></li>
`
}).join('')}
</ul>
`;
this.element.querySelectorAll('button').forEach((button, index) => {
button.addEventListener('click', () => {
store.dispatch('clearItem', { index });
});
});
}
};
每个按钮都附有一个事件,并且它们会触发一个 action,然后由我们的 store 处理 action。
count.js
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Count extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-count')
});
}
render() {
let suffix = store.state.items.length !== 1 ? 's' : '';
let emoji = store.state.items.length > 0 ? '🙌' : '😢';
this.element.innerHTML = `
<small>You've done</small>
${store.state.items.length}
<small>thing${suffix} today ${emoji}</small>
`;
}
}
status.js
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Status extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-status')
});
}
render() {
let suffix = store.state.items.length !== 1 ? 's' : '';
this.element.innerHTML = `${store.state.items.length} item${suffix}`;
}
}
产生交互
需要添加一个初始状态,一些 actions 和一些 mutations。
/js
├── store
└── actions.js
└── index.js
└── mutations.js
└── state.js
└── store.js
state.js
state 保存状态
export default {
items: [
'I made this',
'Another thing'
]
};
每个 action 都会将 payload(关联数据)传递给 mutation,而 mutation 又将数据提交到 store。context 是 Store 类的实例,payload 是触发 action 时传入的。mutations 应该保持简单,因为他们有一个工作:改变 store 的 state。
actions.js
export default {
addItem(context, payload) {
context.commit('addItem', payload);
},
clearItem(context, payload) {
context.commit('clearItem', payload);
}
};
mutations.js
export default {
addItem(state, payload) {
state.items.push(payload);
return state;
},
clearItem(state, payload) {
state.items.splice(payload.index, 1);
return state;
}
};
通过 index 文件将它们结合到一起。
import actions from './actions.js';
import mutations from './mutations.js';
import state from './state.js';
import Store from './store.js';
export default new Store({
actions,
mutations,
state
});
最后一步
main.js
如果有内容,我们将使用该内容作为 payload(关联数据)触发我们的 addItem action, store 为我们处理它。
import store from './store/index.js';
import Count from './components/count.js';
import List from './components/list.js';
import Status from './components/status.js';
const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');
formElement.addEventListener('submit', evt => {
evt.preventDefault();
let value = inputElement.value.trim();
if (value.length) {
store.dispatch('addItem', value);
inputElement.value = '';
inputElement.focus();
}
});
const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();
countInstance.render();
listInstance.render();
statusInstance.render();
聊聊常见的状态管理
Flux
Flux其实是一种思想,就像MVC,MVVM之类的,给出了一些基本概念。Flux把一个应用分成了4个部分: View Action Dispatcher Store。
View 是要展示数据的,当 Store 发生变化,View 也会跟着改变。Store 发生改变,都会往外面发送一个事件,比如 change,通知所有的订阅者。View 通过订阅也好,监听也好,反正 Store 变了,View 就会变。View 一般都会有用户操作,这个时候就需要修改 Store。
Flux 要求,View 要想修改 Store,必须经过一套流程。
视图先要告诉 Dispatcher,让 Dispatcher dispatch 一个 action,Dispatcher 就像是个中转站,收到 View 发出的 action,然后转发给 Store。
比如新建一个 TODO,View 会发出一个叫 addTodo 的 action 通过 Dispatcher 来转发,Dispatcher 会把 addTodo 这个 action 发给所有的 store,store 就会触发 addTodo 这个 action,来更新数据。数据一更新,那么 View 也就跟着更新了。
Dispatcher 的作用是接收所有的 Action,然后发给所有的 Store。这里的 Action 可能是 View 触发的,也有可能是其他地方触发的,比如测试用例。转发的话也不是转发给某个 Store,而是所有 Store。 Store 的改变只能通过 Action,不能通过其他方式。也就是说 Store 不应该有公开的 Setter,所有 Setter 都应该是私有的,只能有公开的 Getter。具体 Action 的处理逻辑一般放在 Store 里。
Flux的最大特点就是数据都是单向流动的。
Redux
Redux 只有一个 Store,整个应用的数据都在这个 Store 里面。Store 的 State 不能直接修改,每次只能返回一个新的 State。
Redux 通过 createStore 函数来生成 Store。
import { createStore } from 'redux';
const store = createStore(fn);
Store 使用 subscribe 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。这样不管 View 是用什么实现的,只要把 View 的更新函数 subscribe 一下,就可以实现 State 变化之后,View 自动渲染了。比如在 React 里,把组件的render方法或setState方法订阅进去就行。
Action
Redux 里面也有 Action,Action 就是 View 发出的通知,告诉 Store State 要改变。Action 必须有一个 type 属性,代表 Action 的名称,payload 谁需要提交的内容。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
dispatch
View 通过 dispatch 方法发出 Action。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
Reducer
Redux 通过 Reducer 来处理事件。
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
import { createStore } from 'redux';
const store = createStore(reducer);
Redux 有很多的 Reducer,所以需要做拆分。Redux 里每一个 Reducer 负责维护 State 树里面的一部分数据,多个 Reducer 可以通过 combineReducers 方法合成一个根 Reducer,这个根 Reducer 负责维护整个 State。
import { combineReducers } from 'redux';
// 注意这种简写形式,State 的属性名必须与子 Reducer 同名
const chatReducer = combineReducers({
Reducer1,
Reducer2,
Reducer3
})
简单来说,Redux有三大原则: 单一数据源,State 是只读的,使用纯函数来执行修改。
在实际项目中,一般都会有同步和异步操作。在 Redux 中,同步的表现就是:Action 发出以后,Reducer 立即算出 State。那么异步的表现就是:Action 发出以后,过一段时间再执行 Reducer。
Redux 引入了中间件 Middleware 的概念,提供了一个 applyMiddleware 方法来应用中间件。
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
这个方法主要就是把所有的中间件组成一个数组,依次执行。也就是说,任何被发送到 store 的 action 现在都会经过thunk,promise,logger 这几个中间件了。
Redux-thunk
const createFetchDataAction = function (id) {
return function (dispatch, getState) {
// 开始请求,dispatch 一个 FETCH_DATA_START action
dispatch({
type: FETCH_DATA_START,
payload: id
})
api.fetchData(id)
.then(response => {
// 请求成功,dispatch 一个 FETCH_DATA_SUCCESS action
dispatch({
type: FETCH_DATA_SUCCESS,
payload: response
})
})
.catch(error => {
// 请求失败,dispatch 一个 FETCH_DATA_FAILED action
dispatch({
type: FETCH_DATA_FAILED,
payload: error
})
})
}
}
//reducer
const reducer = function (oldState, action) {
switch (action.type) {
case FETCH_DATA_START:
// 处理 loading 等
case FETCH_DATA_SUCCESS:
// 更新 store 等
case FETCH_DATA_FAILED:
// 提示异常
}
}
Redux-promise
const FETCH_DATA = 'FETCH_DATA'
//action creator
const getData = function (id) {
return {
type: FETCH_DATA,
payload: api.fetchData(id) // 直接将 promise 作为 payload
}
}
//reducer
const reducer = function (oldState, action) {
switch (action.type) {
case FETCH_DATA:
if (action.status === 'success') {
// 更新 store 等处理
} else {
// 提示异常
}
}
}
Redux-saga
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
Vuex
Store
每一个 Vuex 里面有一个全局的 Store,包含着应用中的状态 State,这个 State 只是需要在组件中共享的数据。
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count
}
}
}
State 改变,View 就会跟着改变,这个改变利用的是 Vue 的响应式机制。
Mutation
State 不能直接改,更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type) 和 一个回调函数 (handler)。mutation 是必须同步的,不同步修改的话,会不知道改变什么时候发生,也很难确定先后顺序,A、B两个 mutation,调用顺序可能是 A -> B,但是最终改变 State 的结果可能是 B -> A。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment(state) {
// 变更状态
state.count++
}
}
})
触发 mutation 事件的方式不是直接调用,比如 increment(state) 是不行的,而要通过 store.commit 方法:
store.commit('increment')
Action
View 通过 store.dispatch('increment') 来触发某个 Action,Action 里面不管执行多少异步操作,完事之后都通过 store.commit('increment') 来触发 mutation,一个 Action 里面可以触发多个 mutation。
Dva
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return {
...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1 };
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({ type: 'add' }) });
},
},
});
Mobx
通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。
简单点来讲:状态只要一变,其他用到状态的地方就都跟着自动变。