浅谈前端响应式设计(一)
现实世界有很多是以响应式的方式运作的,例如我们会在收到他人的提问,然后做出响应,给出相应的回答。在开发过程中笔者也应用了大量的响应式设计,积累了一些经验,希望能抛砖引玉。
响应式编程(Reactive Programming)和普通的编程思路的主要区别在于,响应式以推(push
)的方式运作,而非响应式的编程思路以拉(pull
)的方式运作。例如,事件就是一个很常见的响应式编程,我们通常会这么做:
button.on('click', () => { // ... })
而非响应式方式下,就会变成这样:
while (true) { if (button.clicked) { // ... } }
显然,无论是代码的优雅度还是执行效率上,非响应式的方式都不如响应式的设计。
Event Emitter
Event Emitter
是大多数人都很熟悉的事件实现,它很简单也很实用,我们可以利用Event Emitter
实现简单的响应式设计,例如下面这个异步搜索:
class Input extends Component { state = { value: '' } onChange = e => { this.props.events.emit('onChange', e.target.value) } afterChange = value => { this.setState({ value }) } componentDidMount() { this.props.events.on('onChange', this.afterChange) } componentWillUnmount() { this.props.events.off('onChange', this.afterChange) } render() { const { value } = this.state return ( <input value={value} onChange={this.onChange} /> ) } } class Search extends Component { doSearch = (value) => { ajax(/* ... */).then(list => this.setState({ list })) } componentDidMount() { this.props.events.on('onChange', this.doSearch) } componentWillUnmount() { this.props.events.off('onChange', this.doSearch) } render() { const { list } = this.state return ( <ul> {list.map(item => <li key={item.id}>{item.value}</li>)} </ul> ) } }
这里我们会发现用
Event Emitter
的实现有很多缺点,需要我们手动在 componentWillUnmount
里进行资源的释放。它的表达能力不足,例如我们在搜索时需要聚合多个数据源的时候: class Search extends Component { foo = '' bar = '' doSearch = () => { ajax({ foo, bar }).then(list => this.setState({ list })) } fooChange = value => { this.foo = value this.doSearch() } barChange = value => { this.bar = value this.doSearch() } componentDidMount() { this.props.events.on('fooChange', this.fooChange) this.props.events.on('barChange', this.barChange) } componentWillUnmount() { this.props.events.off('fooChange', this.fooChange) this.props.events.off('barChange', this.barChange) } render() { // ... } }
显然开发效率很低。
Redux
Redux
采用了一个事件流的方式实现响应式,在Redux
中由于reducer
必须是纯函数,因此要实现响应式的方式只有订阅中或者是在中间件中。
如果通过订阅store
的方式,由于Redux
不能准确拿到哪一个数据放生了变化,因此只能通过脏检查的方式。例如:
function createWatcher(mapState, callback) { let previousValue = null return (store) => { store.subscribe(() => { const value = mapState(store.getState()) if (value !== previousValue) { callback(value) } previousValue = value }) } } const watcher = createWatcher(state => { // ... }, () => { // ... }) watcher(store)
这个方法有两个缺点,一是在数据很复杂且数据量比较大的时候会有效率上的问题;二是,如果mapState
函数依赖上下文的话,就很难办了。在react-redux
中,connect
函数中mapStateToProps
的第二个参数是props
,可以通过上层组件传入props
来获得需要的上下文,但是这样监听者就变成了React
的组件,会随着组件的挂载和卸载被创建和销毁,如果我们希望这个响应式和组件无关的话就有问题了。
另一种方式就是在中间件中监听数据变化。得益于Redux
的设计,我们通过监听特定的事件(Action)就可以得到对应的数据变化。
const search = () => (dispatch, getState) => { // ... } const middleware = ({ dispatch }) => next => action => { switch action.type { case 'FOO_CHANGE': case 'BAR_CHANGE': { const nextState = next(action) // 在本次dispatch完成以后再去进行新的dispatch setTimeout(() => dispatch(search()), 0) return nextState } default: return next(action) } }
这个方法能解决大多数的问题,但是在Redux
中,中间件和reducer
实际上隐式订阅了所有的事件(Action),这显然是有些不合理的,虽然在没有性能问题的前提下是完全可以接受的。
面向对象的响应式
ECMASCRIPT 5.1
引入了getter
和setter
,我们可以通过getter
和setter
实现一种响应式。
class Model { _foo = '' get foo() { return this._foo } set foo(value) { this._foo = value this.search() } search() { // ... } } // 当然如果没有getter和setter的话也可以通过这种方式实现 class Model { foo = '' getFoo() { return this.foo } setFoo(value) { this.foo = value this.search() } search() { // ... } }
Mobx
和Vue
就使用了这样的方式实现响应式。当然,如果不考虑兼容性的话我们还可以使用Proxy
。
当我们需要响应若干个值然后得到一个新值的话,在Mobx
中我们可以这么做:
class Model { @observable hour = '00' @observable minute = '00' @computed get time() { return `${this.hour}:${this.minute}` } }
Mobx
会在运行时收集time
依赖了哪些值,并在这些值发生改变(触发setter
)的时候重新计算time
的值,显然要比EventEmitter
的做法方便高效得多,相对Redux
的middleware
更直观。
但是这里也有一个缺点,基于getter
的computed
属性只能描述y = f(x)
的情形,但是现实中很多情况f
是一个异步函数,那么就会变成y = await f(x)
,对于这种情形getter
就无法描述了。
对于这种情形,我们可以通过Mobx
提供的autorun
来实现:
class Model { @observable keyword = '' @observable searchResult = [] constructor() { autorun(() => { // ajax ... }) } }
由于运行时的依赖收集过程完全是隐式的,这里经常会遇到一个问题就是收集到意外的依赖:
class Model { @observable loading = false @observable keyword = '' @observable searchResult = [] constructor() { autorun(() => { if (this.loading) { return } // ajax ... }) } }
显然这里
loading
不应该被搜索的 autorun
收集到,为了处理这个问题就会多出一些额外的代码,而多余的代码容易带来犯错的机会。 或者,我们也可以手动指定需要的字段,但是这种方式就不得不多出一些额外的操作: class Model { @observable loading = false @observable keyword = '' @observable searchResult = [] disposers = [] fetch = () => { // ... } dispose() { this.disposers.forEach(disposer => disposer()) } constructor() { this.disposers.push( observe(this, 'loading', this.fetch), observe(this, 'keyword', this.fetch) ) } } class FooComponent extends Component { this.mode = new Model() componentWillUnmount() { this.state.model.dispose() } // ... }
而当我们需要对时间轴做一些描述时,Mobx
就有些力不从心了,例如需要延迟5秒再进行搜索。
在下一篇博客中,将介绍Observable
处理异步事件的实践。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
漫谈OceanBase 列式存储
列式存储主要的目的有两个: 大部分OLAP查询只需要读取部分列而不是全部列数据,列式存储可以避免读取无用数据; 将同一列的数据在物理上存放在一起,能够极大地提高数据压缩率。 OLAP和OLTP OLAP,也叫联机分析处理(Online Analytical Processing)系统,有的时候也叫DSS决策支持系统,就是我们说的数据仓库。在这样的系统中,语句的执行量不是考核标准,因为一条语句的执行时间可能会非常长,读取的数据也非常多。所以,在这样的系统中,考核的标准往往是磁盘子系统的吞吐量(带宽),如能达到多少MB/s的流量。 在OLAP系统中,常使用分区技术、并行技术。 分区技术在OLAP系统中的重要性主要体现在数据库管理上,比如数据库加载,可以通过分区交换的方式实现,备份可以通过备份分区表空间实现,删除数据可以通过分区进行删除,至于分区在性能上的影响,它可以使得一些大表的扫描变得很快(只扫描单个分区)。另外,如果分区结合并行的话,也可以使得整个表的扫描会变得很快。总之,分区主要的功能是管理上的方便性,它并不能绝对保证查询性能的提高,有时候分区会带来性能上的提高,有时候会降低。 在O...
- 下一篇
Dubbo+zookeeper实现分布式服务框架
什么是Dubbo?? Dubbo也是一套微服务框架,他与SpringCloud的区别就是,他支持多种协议,而SpringCloud只支持Http协议。如果没有分布式,那么他是不存在的。 Dubbo底层架构图 Dubbo底层 首先Provider生成服务将服务注册到zookeeper(具体实现下面有代码),然后zookeeper接收到过后底层会触发zookeeper监听事件(不懂请看前一节),然后告诉Consumer可以消费了,但是Provider关闭过后zookeeper不会删除节点,因为是存储的持久化节点,不是临时节点。然后会有一个专门的模块来监听服务的调用,统计模块调用次数和反馈信息。 Dubbo有哪些作用 ①:Dubbo有服务治理的能力:透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单 配置,没有任何API侵入。 ②:集群容错:软负载均衡及容错机制,可在内网替代F5等硬件负鞭均衡器,降低成本,减少单点。 ③:自动发现:服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。 另外:Dubbo采用...
相关文章
文章评论
共有0条评论来说两句吧...