Redux 使用可变数据结构
不可变数据结构 可调式、可预测、可测试,与可变数据结构的响应式、数据修改简单都很好。
当希望使用 reduxDevTools 一键恢复、回溯页面状态时,我们想使用 redux;当频繁修改复杂对象、使用 ts 希望自动提示时,我们想使用 mobx。
1 合二为一
redux 核心能力是不可变数据带来的,mobx 的核心能力是可变数据带来的,如果使用动态修改数据的方式,使用 immutablejs 将其低成本转化为不可变数据,就可以接入 redux 并享受其生态了。
redux其 store 的代码如下:
import { observable } from "dynamic-object"
export default class TODO {
private store = observable({
todos: [{
text: "learn Redux",
completed: false,
id: 0
}]
})
public addTodo(text: string) {
const id = this.store.todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1
this.store.todos.unshift({
id,
text,
completed: false
})
}
}
已经能看到有意思的东西了,我们将 dynamic-object
包装后的动态对象作为 store,直接修改它,但最终转化成了 redux 不可变数据,并且享受 redux-dev-tools 带来的便利。
dynamic-object 与 mobx 理念、功能很像,内部也没有使用 mobx,其特点是不兼容 IE11,但简化了很多 mobx api,使用起来也更加顺手,可以看看 这个在线Demo 体会一下与 mobx 的区别。
2 结合两者特性
将两者碰撞在一起,特性一定会有所取舍。
2.1 依赖注入 or 单一 store
如果纯粹使用 dynamic-object,我们可以利用依赖注入来实现 action 同时操作多个 store 的能力,下面用伪代码描述,可以参考 完整 demo:
class UserAction {
@inject(UserStore) userStore: UserStore
@inject(ArticleStore) articleStore: ArticleStore
updateUser (name: string) {
const user = this.userStore.getUserByName(name)
this.articleStore.addArticle({
userId: user.id, // title:
})
user.updateArticle()
}
}
但由于 redux 使用了 combineReducers
聚合成一个大 store,每个 reducer 仅能接触到当前节点的 store,因此上面的用法肯定无法支持。
但是一个 action 操作一个 store 有利于数据之间关系的隔离,也利于我们合理规划数据流,因此依赖注入多个 store 的能力可以抛弃掉,使用 redux 的 action 操作单一 store 的思想。
因此可以干脆将 store 内置到 action 中,相当于 initState,action 对 store 的修改可以看作 reducer 使用不可变结构处理了数据,通过 magic 返回了新的 immutable 数据:
import { observable } from "dynamic-object"
export default class User {
private store = observable({
name: "小明"
})
public changeName(name: string) {
this.store.name = name
}
}
以上处理方式,和 redux 如下处理等价:
const initState = {
name: "小明"
}
function userReducer(state = initState, action) {
switch (action.type) {
case "user.changeName":
return {
...state,
name: action.payload
}
default:
return state
}
}
2.2 mutable or immutable
对于简单情况,redux 的 reducer 看起来并不是很复杂,但是在处理深层次数据结构,redux 就显得很无力,因此建议我们将 state 尽量打平。
但并不是所有情况都适合打平,在重前端的场景,所有数据都存储在 store 中,合理的数据结构描述显然比打平结构更重要。
比如下面是 mutable 和 immutable 对复杂数据修改的对比:
const addBuildings = (currentPlanetIndex, building) => {
this.gameUser.planets[currentPlanetIndex].buildings.push(building)
}
const addBuilding = (state, action) => {
return Object.assign({}, state, {
gameUser: Object.assign({}, state.gameUser, {
planets: state.gameUser.planets.map((planet, index) => {
if (index === state.currentPlanetIndex) {
planet.buildings = planet.buildings.concat(action.payload)
return planet
}
return planet
})
})
})
}
对于 immutablejs 的库,我们可以使用 set get 的方式快速修改,但其缺点是没有 IDE 自动提示的支持,后期重构变量名的时候,也很难从字符串中推导出依赖关系。
比较好的整合方式是 mutable 与 immutable 混合:
import { observable } from "dynamic-object"
export default class User {
private gameUser = observable({
planets: []
})
public addBuildings (currentPlanetIndex, building) {
this.gameUser.planets[currentPlanetIndex].buildings.push(building)
}
}
在 observable
的背后,每次 setter 操作,都会调用 Immutablejs 的 api 生成新的不可变对象,当所有操作完毕时,将最终生成的不可变对象返回给 reducer。
3 dynamic-object 使用实例
3.1 定义 store
比如定义存储用户信息的 userStore
:
import { observable, Action } from "dynamic-object"
class User {
store = observable({
name: "小明"
})
@Action setName(name) {
this.store.name = name
}
}
这里需要注意的是,被定义为 observable 的变量会自动成为 initState,当前 Action 仅能操作这个 store,比如 setName
函数。
3.2 redux 桥接
可以使用 createReduxStore
函数生成 redux 的 store 与 action:
import { createReduxStore } from "dynamic-object"
const { store, actions} = createReduxStore({
// 3.1 的 User store
user: User,
// 另一个 store
article: Article
})
以上函数生成了 redux Provider 需要的 store,以及与这个 store 绑定的 actions。actions 的结构如下:
{
user: {
setName: Function
},
article: {
// ...
}
}
3.3 接下来都是 redux 的代码了
import * as React from "react"
import * as ReactDOM from "react-dom"
import { connect, Provider } from "react-redux"
import { store, actions } from "./store"
connect((state, ownProps) => {
return {
name: state.user.name
}
})
class App extends React.PureComponent {
componentWillMount() {
actions.user.setName("test")
}
render() {
return (
<div>
{this.props.name}
</div>
)
}
}
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("react-dom")
)
上述 actions 已经被 dispatch 包过了,所以直接调用即可。查看在线 Demo
3.3.1 dispatch 需要唯一的 type,定义 type 的地方在哪?什么机制保证不会冲突?
比如调用 actions.user.setName("test")
这个函数,那么 type 就是 user.setName
这个字符串,因此不会冲突,而且还可以通过 devTools 快速定位函数位置。
3.3.2 直接改值,redux 可以正确运行吗?
通过 createReduxStore
包装后的 actions,调用后会触发 action,store 中书写的 mutable 代码会自动生成 immutablejs 对象,自动生成 reducer 并返回给 redux,所以和直接使用 redux 没有任何区别,因此可以正确运行。
3.3.3 支持直接在 devTools 里执行 dispatch 吗?
可以,见下图:
dispatch4 总结
dynamic-object 是业余时间写的库,虽然和 mobx 很像,但使用 proxy 解决了很多 Object.defineproperty 无法解决的痛点问题,目前支持下面三种使用方式:
- 结合 react-redux 将 store 层替换为 mutable 方式,提高开发效率,也是本文介绍的方式
- 结合 dynamic-react 在 redux 使用 mutable 数据流
- 结合 dependency-inject 支持依赖注入
各种用法在 github readme 都有 live demo,目前也在当前业务项目中试水,后续会作为底层集成在我们数据团队的数据流解决方案中,敬请期待。