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 吗?

可以,见下图:

dispatch

4 总结

dynamic-object 是业余时间写的库,虽然和 mobx 很像,但使用 proxy 解决了很多 Object.defineproperty 无法解决的痛点问题,目前支持下面三种使用方式:

  • 结合 react-redux 将 store 层替换为 mutable 方式,提高开发效率,也是本文介绍的方式
  • 结合 dynamic-react 在 redux 使用 mutable 数据流
  • 结合 dependency-inject 支持依赖注入

各种用法在 github readme 都有 live demo,目前也在当前业务项目中试水,后续会作为底层集成在我们数据团队的数据流解决方案中,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容