写点什么

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

  • 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:24721

评论

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

【Python】此集合非彼集合

謓泽

5月月更

【多云管理】国内多云管理平台厂家名单汇总

行云管家

云计算 多云管理 多云 云管平台

Alibaba永远滴神!阿里顶级技术官500页网络协议手记,限时开源

Java架构追梦

Java 华为 网络协议 后端开发

2019,不仅是"自由自在",更是AI领域不平凡的一年

Baihai IDP

人工智能 AI

python进阶-装饰器

AIWeker

Python 人工智能 5月月更

未来的神AIoT!全网第一份AIoT系统学习指南,限时开源

Java架构追梦

Java 后端开发 ALOT

中科创达与华为共启边缘计算合作,共建昇腾AI产业,赋能千行百业提质升级

科技热闻

691页,41个版块!终于有阿里大佬可以带我吃透Spring源码了

Java架构追梦

Java spring 后端开发

云天励飞与华为签署合作协议,共同推进昇腾AI产业持续发展

科技热闻

云图说|云数据库RDS跨区域备份

华为云开发者联盟

华为云 云数据库 备份 云数据库RDS 跨区域备份

Alibaba最新神作!耗时182天肝出来1015页分布式全栈手册太香了

Java架构追梦

分布式 java面试 后端开发

看 Amazon 如何通过 Nitro System 构建技术优势

亚马逊云科技 (Amazon Web Services)

Builder 专栏

恭喜 Kvrocks 加入 Apache 软件基金会孵化器

Kvrocks

redis 开源 apache 社区

阿里大牛两万字总结+40张图文详解,不信你还参透不了并发编程

Java架构追梦

高并发 java面试 后端开发

面向对象编程(OOP)

武师叔

5月月更

Swift 首次调试断点慢的问题解法 | 优酷 Swift 实践

阿里巴巴终端技术

ios swift

PingCode Flow技术架构揭秘

PingCode研发中心

北明软件加入昇腾万里伙伴计划,与华为共建昇腾AI生态,共同推动人工智能产业繁荣发展

科技热闻

【案例】锐明技术:灵活部署,实现会话质量和安全的双重保障

行云管家

运维 等保 IT运维 等保2.0

双管齐下, 清华教授亲码JDK和HotSpot源码笔记,一次性学个明白

Java架构追梦

Java 后端开发

SpringBoot集成开源IM框架MobileIMSDK,实现即时通讯IM聊天功能

JackJiang

网络编程 即时通讯 IM TCP协议

如何基于盘古开发框架开发Dubbo微服务网关

码农大熊

微服务架构 网关

浅析分布式系统之体系结构 - 事务与隔离级别(多对象、多操作)上篇

snlfsnef

数据库 架构 设计原则 一致性 事务隔离

Apache ShardingSphere 企业行|走进携程

SphereEx

Apache 数据库 ShardingSphere SphereEx 企业行

WordPress 如何重置密码

海拥(haiyong.site)

5月月更

使用小程序容器技术快速构建智能电视应用平台

Speedoooo

小程序 物联网 移动开发 小程序容器 智能电视

带派!真心被这份阿里大牛开源的“全彩版图解HTTP手册”折服了

Java架构追梦

Java 程序员 后端开发

阿里亿级并发册+机器学习算法+面试册+优化册+代码册 笔记!!!

Java架构追梦

Java 程序员 后端开发

2022年医疗+AI,将会如何蓄力发展?

易观分析

医疗AI

【直播回顾】OpenHarmony知识赋能五期第三课——多媒体整体介绍

OpenHarmony开发者

直播 OpenHarmony 成长计划 多媒体 标准系统

放弃保研和出国,投身基础软件,一位复旦学子的职业选择

TDengine

数据库 tdengine

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