写点什么

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

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

评论

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

Affinity Publisher for Mac(专业排版设计软件) v2.5.2版

Mac相关知识分享

Mac Mac软件 排版设计软件 mac下载

8个实用的Java Streams API

快乐非自愿限量之名

Java API 开发语言

ForkLift for Mac(文件管理程序) v4.1.3版

Mac相关知识分享

软件 文件管理 mac软件下载 Mac办公软件

SpaceX 星舰周四晚八点再试飞;Backseat AI 发布《英雄联盟》实时辅导工具丨 RTE 开发者日报 Vol.219

声网

低代码开发:企业OA低成本数字化转型的新引擎

EquatorCoco

低代码 OA

京东商品评论数据接口(JD.item_review)丨京东平台实时API接口指南

tbapi

京东API接口 京东商品评论接口 京东商品评论采集

PostgreSQL 17 Beta1 发布,酷克数据再次贡献核心力量

酷克数据HashData

TechSmith Snagit for mac(强大的屏幕截图软件) v2023.3.1中文版

Mac相关知识分享

Mac 截图 Mac软件 截图软件

速卖通商品详情API(aliexpress.item_get)返回值详解与实战

技术冰糖葫芦

API 接口 API 文档 API 策略

如何把域名解析到自己的网站?怎么设置域名解析?

国科云

摄影师必备软件XnViewMP for Mac(图片浏览查看器) v1.7.2版

Mac相关知识分享

Mac软件 mac软件下载 图像处理软件

2024-06-05:用go语言,给定三个正整数 n、x 和 y, 描述一个城市中由 n 个房屋和 n 条街道连接的情况。 城市中存在一条额外的街道连接房屋 x 和房屋 y。 需要计算对于每个街道数(

福大大架构师每日一题

福大大架构师每日一题

数据库市场或迎变局 天翼云TeleDB打造企业数据管理最优解

Geek_2d6073

抖音面试:说说延迟任务的调度算法?

王磊

复盘自研产品,总结技术实践

小院里的霍大侠

「C++」深度分析C++中i++与++i的区别 _

不在线第一只蜗牛

c++ 编程 开发语言

被黑客入侵的 DMM 比特币为比特币购买获得 3.2 亿美元

区块链开发团队DappNetWork

Wi-Fi 6E vs. Wi-Fi 7: Which is the Best Fit for Your Infrastructure?

wallyslilly

IPQ8072 ipq9574

CoT思维链 , 大模型“智能涌现”的关键

澳鹏Appen

大模型 模型推理 AGI 智能涌现 思维链

淘宝/天猫商品详情API接口与数据挖掘技术的结合应用

技术冰糖葫芦

API 接口 API 文档 API 策略

软件测试学习笔记丨Vue常用指令-条件渲染(v-if)

测试人

软件测试

为什么身份控制是确保API接口访问安全的关键?

幂简集成

安全 API 身份控制

基于阿里云服务网格流量泳道的全链路流量管理(三):无侵入式的宽松模式泳道

阿里巴巴云原生

阿里云 云原生 服务网格

再见,PCA 主成分分析!

EquatorCoco

Python 人工智能 机器学习

LPS完成战略性收购 增强数据实践和营销云能力

财见

政界人士敦促拜登从尼日利亚带回币安高管

区块链开发团队DappNetWork

未来5年,只有这种产品团队才能开启上帝视角【玩转IPD】

IPD产品研发管理

产品 项目管理 产品开发 软件研发

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