写点什么

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

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

评论

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

架构师训练营—第九周作业

Geek_shu1988

架构师训练营 1 期第 9 周:性能优化(三)- 总结

piercebn

极客大学架构师训练营

秒杀系统

橘子皮嚼着不脆

架构师训练营第二期 Week 5 总结

bigxiang

极客大学架构师训练营

我的亲历:一行代码,百万人民币打水漂

白色蜗牛

Java 程序员 架构 程序人生 职场

【喜讯】Apache DolphinScheduler 荣获 “2020 年度十大开源新锐项目”

代立冬

Apache 大数据 开源 DolphinScheduler Apache DolphinScheduler

架构师训练营 week5 课后作业

花果山

极客大学架构师训练营

真零基础Python开发web

MySQL从删库到跑路

Python django Web bottle

【架构师训练营第 1 期 09 周】 学习总结

Bear

极客大学架构师训练营

5G+工业互联网的中国登山队,如何攀跃“产业化”山峦?

脑极体

架构师训练营第 9 周作业

netspecial

极客大学架构师训练营

架构师训练营—第九周学习总结

Geek_shu1988

架构师训练营 - 第九周总结

一个节点

极客大学架构师训练营

一次用户故事拆(SPIDR)法实践

Bruce Talk

Agile 用户故事 User Story

二分法求平方根,swift面向协议编程protocol从入门到精通、《格局》吴军著读后感、John 易筋 ARTS 打卡 Week 27

John(易筋)

collection ARTS 打卡计划 格局 吴军 李嘉图定律 面向协议protocol编程

Java 中常见的细粒度锁实现

rookiedev

Java 多线程 细粒度锁

架构师训练营第二期 Week 5 作业

bigxiang

极客大学架构师训练营

应届秋招生,熬夜吃透华为架构师这份‘典藏级’计算机网络+计算机操作系统,成功上岸腾讯

网络协议 编程之路 计算机知识

InfoQ 写作平台的魔力

Yolanda

【架构师训练营第 1 期 09 周】 作业

Bear

极客大学架构师训练营

架构师训练营第 9 周课后练习

叶纪想

极客大学架构师训练营

能源区块链研究|区块链与核电安全

CECBC

区块链 核电

第五周总结

孤星

架构师训练营第 9 周学习总结

netspecial

极客大学架构师训练营

架构师训练营 week5 学习总结

花果山

极客大学架构师训练营

技术选型总结一

Mars

技术选型

架构师训练营 - 第九周作业

一个节点

极客大学架构师训练营

Week5 作业1

Sean Chen

JVM垃圾回收原理,秒杀系统架构方案

garlic

极客大学架构师训练营

架构师训练营第九周课程笔记及心得

Airs

【架构师训练营】第九周作业:性能优化

MindController

秒杀系统

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