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.