现实世界有不少是以响应式的方式运做的,例如咱们会在收到他人的提问,而后作出响应,给出相应的回答。在开发过程当中我也应用了大量的响应式设计,积累了一些经验,但愿能抛砖引玉。react
响应式编程(Reactive Programming)和普通的编程思路的主要区别在于,响应式以推(push
)的方式运做,而非响应式的编程思路以拉(pull
)的方式运做。例如,事件就是一个很常见的响应式编程,咱们一般会这么作:ajax
button.on('click', () => {
// ...
})
复制代码
而非响应式方式下,就会变成这样:编程
while (true) {
if (button.clicked) {
// ...
}
}
复制代码
显然,不管在是代码的优雅度仍是执行效率上,非响应式的方式都不如响应式的设计。redux
Event Emitter
是大多数人都很熟悉的事件实现,它很简单也很实用,咱们能够利用Event Emitter
实现简单的响应式设计,例以下面这个异步搜索:异步
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> ) } } 复制代码
这里咱们会发现用Event Emitter
的实现有不少缺点,须要咱们手动在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
中因为reducer
必须是纯函数,所以要实现响应式的方式只有订阅中或者是在中间件中。fetch
若是经过订阅store
的方式,因为Redux
不能准确拿到哪个数据放生了变化,所以只能经过脏检查的方式。例如:this
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
的组件,会随着组件的挂载和卸载被建立和销毁,若是咱们但愿这个响应式和组件无关的话就有问题了。spa
另外一种方式就是在中间件中监听数据变化。得益于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),这显然是有些不合理的,虽然在没有性能问题的前提下是彻底能够接受的。
ECMASCRIPT 5.1
引入了getter
和setter
,咱们能够经过getter
和setter
实现一种响应式。
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() {
// ...
}
}
复制代码
Mobx
和Vue
就使用了这样的方式实现响应式。固然,若是不考虑兼容性的话咱们还能够使用Proxy
。
当咱们须要响应若干个值而后获得一个新值的话,在Mobx
中咱们能够这么作:
class Model {
@observable hour = '00'
@observable minute = '00'
@computed get time() {
return `${this.hour}:${this.minute}`
}
}
复制代码
Mobx
会在运行时收集time
依赖了哪些值,并在这些值发生改变(触发setter
)的时候从新计算time
的值,显然要比EventEmitter
的作法方便高效得多,相对Redux
的middleware
更直观。
可是这里也有一个缺点,基于getter
的computed
属性只能描述y = f(x)
的情形,可是现实中不少状况f
是一个异步函数,那么就会变成y = await f(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
处理异步事件的实践。