-
Notifications
You must be signed in to change notification settings - Fork 1
About
对于一个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概念)。
假设我们想要在帖子详情页面增加一个只看楼主的功能,也就是说我们希望得到一个衍生数据,它依赖于两个原始数据:post和replies:
getRepliesFiltered(post, replies) {
return replies.filter(reply => (
reply.user.id === post.user.id
))
}如何实现?
在dva中,可能有这几种思路:
- 在
postnamespace下,和replies并列的放置一个repliesFiltered,每次更新replies的时候,去同步更新repliesFiltered。可是我们怎样确保所有更新replies和post的地方一定都会更新repliesFiltered,万一有其他人不小心更新到了replies却没有手动更新repliesFiltered怎么办?也许可以写一个updatePost的reducer,但是,如何确保所有人都一定会通过它来更新replies?如何避免直接对replies进行操作?除此以外,用户没有打开“只看楼主”的开关,也就并没有用到repliesFiltered的数据,那此时我们为什么还要计算repliesFiltered呢? - 在组件中计算
repliesFiltered,例如通过getDerivedStateFromProps。但是,这样做是不是会在每次props更新时都重新计算repliesFiltered,从而会产生不必要的性能开销?如果有N个这个组件的实例,是不是会重复N次计算?如果有多个类型的组件都用到这个数据,计算repliesFiltered逻辑如何进行复用?
个人认为,衍生数据应该满足三个特性:
- 响应式,应该会在它依赖的数据变化时自动更新。
- 自动缓存,只有在它依赖的数据变化时才进行更新。
- 懒加载,在我们没有用到这个数据的时候,它只存在对应的逻辑,但没有真正的计算。
在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$这条语句中,也就是说,我们只需要关心:如何通过它的依赖数据产生这个衍生数据,而完全不需要关心这些依赖数据何时发生了变更,发生了什么样的变更。