文 | 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引入了 getter和 setter,他们可以通过 getter和 setter实现一种积极响应式。
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() {
// …
}
}
Mobx和 Vue就使用了这种的形式实现积极响应式。当然,如果不考虑兼容性的话他们还可以使用 Proxy。
当他们需要积极响应若干个值接着得到一个新值的话,在Mobx中他们可以那么做:
classModel {
@observable hour = 00
@observable minute = 00
@computedget time() {
return`${this.hour}:${this.minute}`
}
}
Mobx会在运行时收集 time依赖了哪些值,并在这些值发生改变(触发 setter)的时候重新计算 time的值,显然要比 EventEmitter的做法方便高效得多,相对Redux的 middleware更直观。
但是这里也有两个缺点,基于 getter的 computed属性只能描述 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处理异步该事件的实践。