免费下载案例集|20+数字化领先企业人才培养实践经验 了解详情
写点什么

浅谈前端响应式设计(一)

  • 2020-03-08
  • 本文字数:3164 字

    阅读完需:约 10 分钟

浅谈前端响应式设计(一)

现实世界有很多是以响应式的方式运作的,例如我们会在收到他人的提问,然后做出响应,给出相应的回答。在开发过程中我也应用了大量的响应式设计,积累了一些经验,希望能抛砖引玉。


响应式编程(Reactive Programming)和普通的编程思路的主要区别在于,响应式以推( push)的方式运作,而非响应式的编程思路以拉( pull)的方式运作。例如,事件就是一个很常见的响应式编程,我们通常会这么做:


button.on('click', () => {  // ...})
复制代码


而非响应式方式下,就会变成这样:


while (true) {  if (button.clicked) {    // ...  }}
复制代码


显然,无论在是代码的优雅度还是执行效率上,非响应式的方式都不如响应式的设计。

Event Emitter

EventEmitter是大多数人都很熟悉的事件实现,它很简单也很实用,我们可以利用 EventEmitter实现简单的响应式设计,例如下面这个异步搜索:


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> ) }}
复制代码


这里我们会发现用 EventEmitter的实现有很多缺点,需要我们手动在 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),这显然是有些不合理的,虽然在没有性能问题的前提下是完全可以接受的。

面向对象的响应式

ECMASCRIPT5.1引入了 gettersetter,我们可以通过 gettersetter实现一种响应式。


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() { // ... }}
复制代码


MobxVue就使用了这样的方式实现响应式。当然,如果不考虑兼容性的话我们还可以使用 Proxy


当我们需要响应若干个值然后得到一个新值的话,在 Mobx中我们可以这么做:


class Model {  @observable hour = '00'  @observable minute = '00'
@computed get time() { return `${this.hour}:${this.minute}` }}
复制代码


Mobx会在运行时收集 time依赖了哪些值,并在这些值发生改变(触发 setter)的时候重新计算 time的值,显然要比 EventEmitter的做法方便高效得多,相对 Reduxmiddleware更直观。


但是这里也有一个缺点,基于 gettercomputed属性只能描述 y=f(x)的情形,但是现实中很多情况 f是一个异步函数,那么就会变成 y=awaitf(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处理异步事件的实践。


2020-03-08 19:24572

评论

发布
暂无评论
发现更多内容

「智造」第8期:浅谈国内外对智能制造体系的定义和标准

用友BIP

智能制造

九章云极DataCanvas大模型系列成果发布会重磅来袭,诚邀见证!

九章云极DataCanvas

软件测试/测试开发丨人工智能在软件测试领域的崭新前景

测试人

人工智能 软件测试

Navicat Premium for Mac(多协议数据库管理工具) 16.2.9永久激活版

mac

数据库管理工具 苹果mac Windows软件 Navicat Premium 16

SQL高级之慢查询日志?

百度搜索:蓝易云

MySQL sql 云计算 Linux 运维

Flask新手教程。

百度搜索:蓝易云

Python sql Linux Web 云服务器

科兴未来|全球伯乐计划启动!

科兴未来News

外贸网站被谷歌收录的方法

九凌网络

一点资讯“一号市集”广州开市 赋能车企营销新市景

科技热闻

如何构建更简洁的前端架构?

互联网工科生

前端 前端架构

纯CSS实现炫酷文本阴影效果

南城FE

CSS 前端 动画 阴影

Redis 的集群模式实现高可用

树上有只程序猿

redis

【2023云栖】陈守元:阿里云开源大数据产品年度发布

阿里云大数据AI技术

Redis常用的八种场景

高端章鱼哥

redis

OpenHarmony Meetup北京站招募令

OpenHarmony开发者

中台架构下的性能测试实践方法

老张

性能测试 中台战略 全链路压测 稳定性保障

如何建设一个高效的中英文外贸网站?

九凌网络

JAVA中的函数接口,你都用过吗

不在线第一只蜗牛

Java 函数 接口设计

Microsoft Remote Desktop for Mac 远程桌面连接工具

彩云

远程桌面连接 microsoft remote desktop

.NET8.0 AOT 经验分享 - 专项测试各大 ORM 是否支持

EquatorCoco

.net ORM AOT

浅谈前端响应式设计(一)_文化 & 方法_jinzhixin_InfoQ精选文章