Skip to content
awmleer edited this page Nov 8, 2018 · 3 revisions

它是什么?为什么我要用它?

react应用的分层

对于一个react SPA来说,可以粗略地分为三层

  • view层:react + react-dom
  • model层:redux(dva)、MobX
  • service层

jorum所做的事情是提供一套model的层的解决方案,因此,下面的对比大多是和dva/redux相比较的。

为了便于理解,让我们假设我们正在开发一个简易的论坛应用。

全局状态?局部状态?

在dva中,model是全局的,这意味着,假如我们有一个帖子列表页面PostListPage,那么按照dva的设计思路,我们应该把posts数组放在namespace为post的model中:

{
  namespace: 'post',
  model: {
    posts: [...]
  }
}

这当然没什么问题,但是对于帖子详情页面PostDetailPage,难道我们也应该把post放在dva的model中么?

{
  namespace: 'post',
  model: {
    posts: [...],
    currentPost: {...}
  }
}

除了PostDetailPage和它的下级组件,其他组件并不会关心currentPost是什么,那么把currentPost放在这里,是不是一种全局空间的滥用?

此外,按照currentPost的语义,我们是不是应该在离开PostDetailPage的时候,重置currentPost?因为如果我们不手动重置的话,当用户回退到列表页面然后再点开另一个帖子的时候,会在api请求尚未完成之前,短暂的显示上一篇浏览的帖子内容。然后,我们还需要在组件挂载的时候去触发currentPost的数据更新。

也就是类似这样:

componentDidMount() {
  this.props.dispatch({
    type: 'FetchCurrentPost',
    payload: {
      postId: this.props.match.params.postId
    }
  })
}
componentWillUnmount() {
  this.props.dispatch({type: 'ResetCurrentPost'})
}

但是,为什么我们需要手动的去管理currentPost?如果它的逻辑空间是和PostDetailPage绑定在一起的话,我们可不可以实现,在PostDetailPage挂载的时候,自动去初始化一个当前post相关的逻辑单元,在PostDetailPage销毁的时候,自动的销毁对应的post数据?

这正是jorum致力于实现的方案,数据相关的逻辑被封装成一个个BLoC(Bussiness Logic Component),再把react组件和BLoC耦合起来:

render() {
    <Provider of={PostBloc} args={[this.props.postId]}>
        ...
    </Provider>
}

组件创建,BLoC也跟随着创建,组件销毁,所用到的BLoC也会自动被垃圾回收。这是不是更易于维护一些?

嵌套列表

数据衍生数据

也许我们可以把数据分为两类,一类是原始数据,另一类是衍生数据(类似于vue中的computed概念)。

假设我们想要在帖子详情页面增加一个只看楼主的功能,也就是说我们希望得到一个衍生数据,它依赖于两个原始数据:postreplies

getRepliesFiltered(post, replies) {
  return  replies.filter(reply => (
    reply.user.id === post.user.id
  ))
}

如何实现?

在dva中,可能有这几种思路:

  1. postnamespace下,和replies并列的放置一个repliesFiltered,每次更新replies的时候,去同步更新repliesFiltered。可是我们怎样确保所有更新repliespost的地方一定都会更新repliesFiltered,万一有其他人不小心更新到了replies却没有手动更新repliesFiltered怎么办?也许可以写一个updatePost的reducer,但是,如何确保所有人都一定会通过它来更新replies?如何避免直接对replies进行操作?除此以外,用户没有打开“只看楼主”的开关,也就并没有用到repliesFiltered的数据,那此时我们为什么还要计算repliesFiltered呢?
  2. 在组件中计算repliesFiltered,例如通过getDerivedStateFromProps。但是,这样做是不是会在每次props更新时都重新计算repliesFiltered,从而会产生不必要的性能开销?如果有N个这个组件的实例,是不是会重复N次计算?如果有多个类型的组件都用到这个数据,计算repliesFiltered逻辑如何进行复用?

个人认为,衍生数据应该满足三个特性:

  1. 响应式,应该会在它依赖的数据变化时自动更新。
  2. 自动缓存,只有在它依赖的数据变化时才进行更新。
  3. 懒加载,在我们没有用到这个数据的时候,它只存在对应的逻辑,但没有真正的计算。

在jorum中,这个功能的实现非常简洁明了:

export class PostBloc extends Bloc {
  post$ = new BehaviorSubject(null)
  replies$ = new BehaviorSubject([])
  
  //TODO 需要验证
  repliesFiltered$ = this.post$.pipe(
    switchMap(post => (
      this.replies$.pipe(
        map(replies => {
          replies.filter(reply => reply.user.id === post.user.id)
        })
      )
    ))
  )
}

我们基于post$replies$,创造出了一个新的observable。得益于rx.js的特性,repliesFiltered$已经完美满足了上面提到的三个特性,我们完全不需要自己实现多余的逻辑。

repliesFiltered$数据生成相关的逻辑被完全的包括在了repliesFiltered$这条语句中,也就是说,我们只需要关心:如何通过它的依赖数据产生这个衍生数据,而完全不需要关心这些依赖数据何时发生了变更,发生了什么样的变更。

Clone this wiki locally