On the front-end responsive design (1)

Time:2020-11-27

Many of the real world operates in a responsive way. For example, we receive questions from others, and then respond and give corresponding answers. In the process of development, the author has also applied a lot of responsive design, accumulated some experience, and hope to be able to draw on the jade.

The main difference between reactive programming and common programming is that responsive programming can push(push)The way to operate, rather than responsive programming ideas to pull(pull)How it works. For example, an event is a very common reactive programming. We usually do this:

button.on('click', () => {
    // ...
})

In the non responsive mode, it will be like this:

while (true) {
    if (button.clicked) {
        // ...
    }
}

Obviously, non responsive design is not as good as responsive design in terms of code elegance or execution efficiency.

Event Emitter

Event EmitterIt is a very familiar event implementation for most people. It is very simple and practical, and we can use itEvent EmitterImplement a simple responsive design, 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>
        )
    }
}

We’ll find it useful hereEvent EmitterThere are a lot of shortcomings in the implementation of, we need to manually in thecomponentWillUnmountTo release resources. For example, when we need to aggregate multiple data sources in search:

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() {
        // ...
    }
}

Obviously, the development efficiency is very low.

Redux

ReduxIn this paper, an event flow method is used to implement the responseReduxDue toreducerIt must be a pure function, so the only way to implement responsive is in subscription or middleware.

If through subscriptionstoreBecauseReduxCan not accurately get which data released changes, so only through the dirty check. For example:

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)

This method has two disadvantages: one is the efficiency problem when the data is very complex and the amount of data is relatively large; the other is that ifmapStateIf the function depends on the context, it is very difficult to do. stayreact-reduxMedium,connectFunctionmapStateToPropsThe second parameter of isprops, which can be passed in through the upper layer componentspropsTo get the context you need, but the listener becomesReactThe component will be created and destroyed as the component is mounted and unloaded. If we want this responsive expression to be independent of the component, there will be a problem.

Another way is to monitor data changes in middleware. Benefit fromReduxWe can get the corresponding data changes by listening to specific events (actions).

const search = () => (dispatch, getState) => {
    // ...
}

const middleware = ({ dispatch }) => next => action => {
    switch action.type {
        case 'FOO_CHANGE':
        case 'BAR_CHANGE': {
            const nextState = next(action)
            //After this dispatch is completed, a new dispatch will be carried out
            setTimeout(() => dispatch(search()), 0)
            return nextState
        }
        default:
            return next(action)
    }
}

This method can solve most problems, but in theReduxMiddle, middleware andreducerIn fact, it is unreasonable to implicitly subscribe to all the actions, although it is totally acceptable without performance problems.

Object oriented response

ECMASCRIPT 5.1IntroducedgetterandsetterWe can go throughgetterandsetterImplement a responsive.

class Model {
    _foo = ''

    get foo() {
        return this._foo
    }

    set foo(value) {
        this._foo = value
        this.search()
    }

    search() {
        // ...
    }
}

//Of course, if there is no getter or setter, it can be implemented in this way
class Model {
    foo = ''

    getFoo() {
        return this.foo
    }

    setFoo(value) {
        this.foo = value
        this.search()
    }

    search() {
        // ...
    }
}

MobxandVueThis method is used to implement responsive. Of course, we can use it without considering compatibilityProxy

When we need to respond to several values and then get a new value, theMobxWe can do this:

class Model {
    @observable hour = '00'
    @observable minute = '00'
    
    @computed get time() {
        return `${this.hour}:${this.minute}`
    }
}

MobxWill be collected at run timetimeWhat values are dependent on and when these values change (triggersetter)RecalculatetimeIs obviously better thanEventEmitterThe method is much more convenient and efficientReduxOfmiddlewareMore intuitive.

But there is also a drawback here, based ongetterOfcomputedProperty can only be describedy = f(x)But there are a lot of situations in realityfIs an asynchronous function, then it becomesy = await f(x)In this casegetterIt can’t be described.

In this case, we can pass theMobxProvidedautorunTo achieve:

class Model {
    @observable keyword = ''
    @observable searchResult = []

    constructor() {
        autorun(() => {
            // ajax ...
        })
    }
}

Because the runtime dependency collection process is completely implicit, we often encounter a problem that unexpected dependencies are collected

class Model {
    @observable loading = false
    @observable keyword = ''
    @observable searchResult = []

    constructor() {
        autorun(() => {
            if (this.loading) {
                return
            }
            // ajax ...
        })
    }
}

Obviously hereloadingIt’s not supposed to be searchedautorunCollected, in order to deal with this problem, there will be some extra code, and redundant code is prone to error.
Alternatively, we can manually specify the required fields, but in this way, we have to add some additional operations:

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()
    }

    // ...
}

And when we need to do something about the timeline,MobxFor example, we need to delay the search by 5 seconds.

In the next blog post, we will introduceObservableThe practice of handling asynchronous events.

Original address: https://tech.youzan.com/react…