A lot of the real world works in a responsive way. For example, when we receive a question from someone, we respond and respond accordingly. In the development process, I also applied a large number of responsive design, accumulated some experience, hoping to shed some light.
The main difference between Reactive Programming and normal Programming is that Reactive Programming operates as a push, while non-reactive Programming operates as a pull. For example, events are a very common form of reactive programming, and we usually do this:
button.on('click', () = > {// ...
})
Copy the code
Instead of being responsive, it looks like this:
while (true) {
if (button.clicked) {
// ...}}Copy the code
Clearly, non-responsive design is inferior to responsive design in terms of both code elegance and execution efficiency.
Event Emitter
Event Emitter is an Event Emitter implementation that many people are familiar with. It is simple and useful. We can use Event Emitter to implement simple, responsive designs, such as the following asynchronous search:
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>)}}Copy the code
Here we will find that the Implementation of Event Emitter has many disadvantages, requiring us to manually release resources in componentWillUnmount. Its expression ability is insufficient, for example, when we need to aggregate multiple data sources in search:
class Search extends Component {
foo = ' '
bar = ' '
doSearch = (a)= > {
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() {
// ...}}Copy the code
Obviously the development efficiency is very low.
Redux
Redux adopts an event stream to realize responsiveness. In Redux, reducer must be a pure function, so responsiveness can only be realized in the subscription or middleware.
If you subscribe to a store, Redux can’t get exactly which data changes are made, so you can only do dirty checks. Such as:
function createWatcher(mapState, callback) {
let previousValue = null
return (store) = > {
store.subscribe((a)= > {
const value = mapState(store.getState())
if(value ! == previousValue) { callback(value) } previousValue = value }) } }const watcher = createWatcher(state= > {
// ...= > {}, ()// ...
})
watcher(store)
Copy the code
This method has two disadvantages. One is that it is inefficient when the data is very complex and the data volume is relatively large. Second, if the mapState function is context-dependent, it becomes difficult. In react-redux, the second parameter to mapStateToProps in the connect function is props. The upper component can pass the props to get the required context, but the listener becomes the React component, which is created and destroyed as the component is mounted and unmounted. We have a problem if we want this reactive to be component-independent.
Another approach is to listen for data changes in the middleware. Thanks to Redux’s design, we can listen for specific events (actions) and get corresponding data changes.
const search = (a)= > (dispatch, getState) => {
// ...
}
const middleware = ({ dispatch }) = > next => action= > {
switch action.type {
case 'FOO_CHANGE':
case 'BAR_CHANGE': {
const nextState = next(action)
// Run a new dispatch after the current dispatch is complete
setTimeout((a)= > dispatch(search()), 0)
return nextState
}
default:
return next(action)
}
}
Copy the code
This approach solves most of the problems, but in Redux, the middleware and reducer actually implicitly subscribe to all events (actions), which is obviously not reasonable, although it is perfectly acceptable without performance issues.
Object-oriented responsiveness
ECMASCRIPT 5.1 introduces getters and setters, through which we can implement a reactive form.
class Model {
_foo = ' '
get foo() {
return this._foo
}
set foo(value) {
this._foo = value
this.search()
}
search() {
// ...}}// This can be done without getters and setters
class Model {
foo = ' '
getFoo() {
return this.foo
}
setFoo(value) {
this.foo = value
this.search()
}
search() {
// ...}}Copy the code
Mobx and Vue use this approach to achieve responsiveness. Of course, we can also use proxies if compatibility is not an issue.
When we need to respond to several values and get a new value, in Mobx we can do this:
class Model {
@observable hour = '00'
@observable minute = '00'
@computed get time() {
return `The ${this.hour}:The ${this.minute}`}}Copy the code
Mobx collects values that time depends on at run time and recalcates time values when those values change (triggering setters), which is much more convenient and efficient than EventEmitter and more intuitive than Redux’s Middleware.
However, there is a drawback here. Computed properties based on getters can only describe y = f(x), but in many cases f is an asynchronous function, so it becomes y = await f(x), for which the getter cannot describe it.
For this, we can use Mobx’s Autorun:
class Model {
@observable keyword = ' '
@observable searchResult = []
constructor() {
autorun((a)= > {
// ajax ...}}})Copy the code
Since the dependency collection process at runtime is completely implicit, there is often a problem of collecting unexpected dependencies:
class Model {
@observable loading = false
@observable keyword = ' '
@observable searchResult = []
constructor() {
autorun((a)= > {
if (this.loading) {
return
}
// ajax ...}}})Copy the code
Obviously loading should not be collected by searching Autorun, so there is some extra code to deal with this problem, and the extra code is easy to make mistakes. Alternatively, we could specify the required fields manually, but that would require some extra operations:
class Model {
@observable loading = false
@observable keyword = ' '
@observable searchResult = []
disposers = []
fetch = (a)= > {
// ...
}
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()
}
// ...
}
Copy the code
Mobx struggled when we needed to do some description of the timeline, such as delaying the search by 5 seconds.
In the next blog post, I’ll cover the practice of an Observable handling asynchronous events.