我们大部分对内产品,都广泛使用了 dob 管理前端数据流,下面隆重介绍一下。
dob 是利用 proxy 实现的数据依赖追踪工具,利用 dob-react 与 react 结合。
dob 的核心思想大量借鉴了 mobx,但是从实现原理、使用便捷性,以及调试工具都做了大量优化。
特征
- ✅ 支持
- ❌ 不支持
- 📦 生态支持
- 🤷 不完全支持
功能 | redux | mobx | dob |
---|---|---|---|
异步 | 📦redux-thunk 等 | ✅ | ✅ |
可回溯 | ✅ | 📦 mst | ✅ |
分形 | 🤷 replaceReducer | ✅ | ✅ |
代码精简 | 📦 dva 等 | ✅ | ✅ |
函数式 | ✅ | 🤷 | 🤷 |
面向对象 | 🤷 | ✅ | ✅ |
Typescript 支持 | 🤷 | ✅ | ✅ |
调试工具 | ✅ | ✅ | ✅ |
调试工具 action 与 UI 双向绑定 | ❌ | 🤷 | ✅ |
严格模式 | ✅ | ✅ | |
支持原生 Map 等类型 | ❌ | ✅ | |
observable 语法自然度 | ❌ | ✅ | |
store 规范化 | ✅ | 🤷 | ✅ |
从依赖追踪开始
dob 自己只实现了依赖追踪功能,其特性非常简单,如下示意图+代码所示:
imgimport { observable, observe } from "dob" const obj = observable({ a: 1, b: 1 }) observe(() => { console.log(obj.a) })
一句话描述就是:由
observable
产生的对象,在observe
回调函数中使用,当这个对象被修改时,会重新执行这个回调函数。
与 react 优雅结合
那么利用这个特性,将 observe 换成 react 框架的 render 函数,就变成了下图:
imgimport { observable, observe } from "dob" import { Provider, Connect } from 'dob-react' const obj = observable({ a: 1 }) @Connect class App extends React.Component { render() { return ( <span onClick={() => { this.props.store.a = 2 }}> {this.props.store.a} </span> ) } } ReactDOM.render( <Provider store={obj}> <App/> </Provider> , dom)
这正是 dob-react 做的工作。
上面这种结合随意性太强,不利于项目维护,真正的 dob-react 对 dob 的使用方式做了限制。
全局数据流
为了更好管理全局数据流,我们引入 action、store 的概念,组件只能触发 action,只有 action 内部才能修改 store:
img由于聚合 store 注入到 react 非常简单,只需要 Provider
@Connect
即可,所以组织好 store 与 action 的关系,也就组织好了整个应用结构。
那么如何组织 action、store、react 之间的关系呢?对全局数据流,dob 提供了一种成熟的模式:依赖注入。以下是可维护性良好模式:
imgimport { Action, observable, combineStores, inject } from 'dob' import { Provider, Connect } from 'dob-react' @observable export class UserStore { name = 'bob' } export class UserAction { @inject(UserStore) private UserStore: UserStore; @Action setName () { this.store.name = 'lucy' } } @Connect class App extends React.Component { render() { return ( <span onClick={this.props.UserAction.setName}> {this.props.UserStore.name} </span> ) } } ReactDOM.render( <Provider { ...combineStores({ UserStore, UserAction }) }> <App /> </Provider> , dom)
一句话描述就是:通过
combineStores
聚合 store 与 action,store 通过inject
注入到 action 中被修改,react 组件通过@Connect
自动注入聚合 store。
局部数据流
对于对全局状态不敏感的数据,可以作为局部数据流处理。
@Connect
装饰器如果不带参数,会给组件注入 Provider
所有参数,如果参数是一个对象,除了注入全局数据流,还会把这个对象注入到当前组件,由此实现了局部数据流。
PS: Connect 函数更多用法可以参考文档: dob-react #Connect
结构如下图所示:
imgimport { Action, observable, combineStores, inject } from 'dob' import { Provider, Connect } from 'dob-react' @observable export class UserStore { name = 'bob' } export class UserAction { @inject(UserStore) private UserStore: UserStore; @Action setName () { this.store.name = 'lucy' } } @Connect(combineStores(UserStore, UserAction)) class App extends React.Component { render() { return ( <span onClick={this.props.UserAction.setName}> {this.props.UserStore.name} </span> ) } }
PS: 局部数据流可以替代 setState
管理组件自身状态,每当组件被实例化一次,就会创建一个与之绑定的局部数据流。如果不想使用 react 提供的 setState,可以使用局部数据流替代。
异步 & 副作用
redux 中需要将副作用代码从 reducer 抽离,而 dob 不需要,我们可以如下书写 action:
@Action async getUserInfo() { this.UserStore.loading = true this.UserStore.currentUser = await fetchUser() this.UserStore.loading = false try { this.UserStore.articles = await fetchArticle() } catch(error) { // 静默失败 } }
Devtools
借助 dob-react-devtools 开启调试模式,可以实现类似 redux-devtools 的效果,但,该调试工具具备 action 与 UI 双向可视化绑定 的功能等:
- UI 与 action 绑定:ui 元素触发 rerender 时,自身会高亮,并在左上角显示渲染次数,以及导致其 render 的 action。
- action 与 UI 绑定:展开右侧 action 列表后,通过 hover 可展示因此 action 触发而 rerender 的 UI 元素,高亮出来。
- 搜索、清空等方式管理 action。
- 点击灯泡 开启/关闭 debug 模式。
假设现在有一个文章列表需求,我们创建了 ArticleStore
与 ArticleAction
,ArticleAction
提供了 addArticle, removeArticle, changeArticleTitle 等基础方法。
现在我们开启了调试功能,获得如下 gif 图的效果:
dob-react-devtoolsdob-react-devtools 主要提供了可视化界面展示每个 Action 触发列表,鼠标移动到每个 Action 会高亮对应 rerender 的 UI 元素,UI 元素 render 的时候,左上角工具条也列出了与这个 UI 元素相关的 Action 列表。
更多专业前端知识,请上 【猿2048】www.mk2048.com