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

2023-05-28 0 520

文 | jinzhixin on 后端

现实生活当今世界有许多是以积极响应式的形式运转的,比如他们会在接到别人的发问,接着作出积极响应,得出适当的提问。在合作开发操作过程中我也应用领域了大批的积极响应式结构设计,累积了许多实战经验,期望能抛砖引玉。

积极响应式程式结构设计(Reactive Programming)和一般的程式结构设计路子的主要就差别是,积极响应式以推( push)的形式运转,而非积极响应式的程式结构设计路子昂尚( pull)的形式运转。比如,该事件是两个很常用的积极响应式程式结构设计,他们一般来说会那么做:

button.on(click, () => {

// …

})

而非积极响应式形式下,就会变为这种:

while (true) {

if (button.clicked) {

// …

   }

}

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

Event Emitter

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

classInputextendsComponent {

   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} />

       )

   }

}

classSearchextendsComponent {

   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里进行资源的释放。它的表达能力不足,比如他们在搜索的时候需要聚合多个数据源的时候:

classSearchextendsComponent {

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不能准确拿到哪两个数据放生了变化,因此只能通过脏检查的形式。比如:

functioncreateWatcher(mapState, callback) {

   let previousValue = null

return (store) => {

       store.subscribe(() => {

constvalue = mapState(store.getState())

if (value !== previousValue) {

               callback(value)

           }

           previousValue = value

       })

   }

}

constwatcher = createWatcher(state => {

// …

}, () => {

// …

})

watcher(store)

这个方法有两个缺点,一是在数据很复杂且数据量比较大的时候会有效率上的问题;二是,如果mapState函数依赖上下文的话,就很难办了。在 react-redux中, connect函数中 mapStateToProps的第二个参数是 props,可以通过上层组件传入 props来获得需要的上下文,但是这种监听者就变为了React的组件,会随着组件的挂载和卸载被创建和销毁,如果他们期望这个积极响应式和组件无关的话就有问题了。

另一种形式是在中间件中监听数据变化。得益于 Redux的结构设计,他们通过监听特定的该事件(Action)就可以得到对应的数据变化。

const search = () => (dispatch, getState) => {

// …

}

constmiddleware = ({ dispatch }) => next => action => {

switch action.type {

caseFOO_CHANGE:

caseBAR_CHANGE: {

constnextState = next(action)

// 在本次dispatch完成以后再去进行新的dispatch

           setTimeout(() => dispatch(search()), 0)

returnnextState

       }

default:

return next(action)

   }

}

这个方法能解决大多数的问题,但是在 Redux中,中间件和 reducer实际上隐式订阅了所有的该事件(Action),这显然是有些不合理的,虽然在没有性能问题的前提下是完全可以接受的。

面向对象的积极响应式

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

classModel{

   _foo =

get foo() {

returnthis._foo

   }

set foo(value) {

this._foo = value

this.search()

   }

   search() {

// …

   }

}

// 当然如果没有getter和setter的话也可以通过这种形式实现

classModel {

   foo =

   getFoo() {

returnthis.foo

   }

   setFoo(value) {

this.foo = value

this.search()

   }

   search() {

// …

   }

}

MobxVue就使用了这种的形式实现积极响应式。当然,如果不考虑兼容性的话他们还可以使用 Proxy

当他们需要积极响应若干个值接着得到一个新值的话,在Mobx中他们可以那么做:

classModel {

@observable hour = 00

@observable minute = 00

@computedget time() {

return`${this.hour}:${this.minute}`

   }

}

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

但是这里也有两个缺点,基于 gettercomputed属性只能描述 y=f(x)的情形,但是现实生活中许多情况 f是两个异步函数,那么就会变为 y=awaitf(x),对于这种情形 getter就无法描述了。

对于这种情形,他们可以通过 Mobx提供的 autorun来实现:

classModel {

@observable keyword =

@observable searchResult = []

   constructor() {

       autorun(() => {

// ajax …

       })

   }

}

由于运行时的依赖收集操作过程完全是隐式的,这里经常会遇到两个问题是收集到意外的依赖:

classModel {

@observable loading = false

@observable keyword =

@observable searchResult = []

constructor() {

       autorun(() => {

if (this.loading) {

return

           }

// ajax …

       })

   }

}

显然这里 loading不应该被搜索的 autorun收集到,为了处理这个问题就会多出许多额外的代码,而多余的代码容易带来犯错的机会。

或者,他们也可以手动指定需要的字段,但是这种形式就不得不多出许多额外的操作:

classModel {

@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)

       )

   }

}

classFooComponent extends Component {

this.mode = newModel()

componentWillUnmount() {

this.state.model.dispose()

   }

// …

}

而当他们需要对时间轴做许多描述时, Mobx就有些力不从心了,比如需要延迟5秒再进行搜索。

在下一篇博客中,将介绍 Observable处理异步该事件的实践。

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

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务