Home  >  Article  >  Web Front-end  >  Detailed introduction to front-end responsive programming solutions and their shortcomings (with code)

Detailed introduction to front-end responsive programming solutions and their shortcomings (with code)

不言
不言Original
2018-08-14 15:14:452470browse

What this article brings to you is a detailed introduction to front-end responsive programming and its shortcomings (with code). It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you. help.

Many parts of the real world operate in a responsive manner. For example, we will respond to questions from others and give corresponding answers. During the development process, I also applied a lot of responsive design and accumulated some experience, hoping to inspire others.

The main difference between reactive programming (Reactive Programming) and ordinary programming ideas is that reactive programming operates in a push way, while non-reactive programming ideas operate in a pull way. For example, events are a very common reactive programming. We usually do this:

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

In non-reactive mode, it will become like this:

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

Obviously, no matter whether In terms of code elegance and execution efficiency, non-responsive methods are not as good as responsive designs.

Event Emitter

Event Emitter is an event implementation that most people are familiar with. It is very simple and practical. We can use Event Emitter to implement simple responsiveness. 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(&#39;onChange&#39;, this.doSearch)
    }
    componentWillUnmount() {
        this.props.events.off(&#39;onChange&#39;, this.doSearch)
    }
    render() {
        const { list } = this.state
        return (            <ul>
                {list.map(item => <li key={item.id}>{item.value}</li>)}            </ul>
        )
    }
}

Here we will find that the implementation of Event Emitter has many shortcomings, and we need to manually release resources in componentWillUnmount. Its expressive ability is insufficient, for example, when we need to aggregate multiple data sources when searching:

class Search extends Component {  
    foo = &#39;&#39;
    bar = &#39;&#39;
    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(&#39;fooChange&#39;, this.fooChange)        this.props.events.on(&#39;barChange&#39;, this.barChange)
    }
    componentWillUnmount() {        this.props.events.off(&#39;fooChange&#39;, this.fooChange)        this.props.events.off(&#39;barChange&#39;, this.barChange)
    }
    render() {        // ...
    }
}

Obviously the development efficiency is very low.

Redux

Redux uses an event stream to implement responsiveness. In Redux, since the reducer must be a pure function, the only way to implement responsiveness is through subscription. in or in middleware.

If you subscribe to the store, since Redux cannot accurately get which data has changed, it can only use dirty checking. 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 that there will be efficiency problems when the data is complex and the amount of data is relatively large; Yes, it would be difficult if the mapState function relied on context. In react-redux, the second parameter of mapStateToProps in the connect function is props. Props can be passed in through the upper component to obtain the required context, but in this way the listener becomes a React component and will be mounted as the component is mounted. And unloading is created and destroyed. If we want this responsiveness to be independent of components, there will be a problem.

Another way is to monitor data changes in middleware. Thanks to the design of Redux, we can get the corresponding data changes by listening to specific events (Action).

const search = () => (dispatch, getState) => {  
    // ...}const middleware = ({ dispatch }) => next => action => {  
    switch action.type {        case &#39;FOO_CHANGE&#39;:        case &#39;BAR_CHANGE&#39;: {            const nextState = next(action)            // 在本次dispatch完成以后再去进行新的dispatch
            setTimeout(() => dispatch(search()), 0)            return nextState
        }        default:            return next(action)
    }
}

This method can solve most problems, but in Redux, middleware and reducer actually implicitly subscribe to all events (Action), which is obviously unreasonable, although there is no performance It's perfectly acceptable under the premise of the question.

Object-oriented responsiveness

ECMASCRIPT 5.1 introduces getters and setters, and we can implement a responsive style through getters and setters.

class Model {  
    _foo = &#39;&#39;
    get foo() {        return this._foo
    }
    set foo(value) {        this._foo = value        this.search()
    }
    search() {        // ...
    }
}// 当然如果没有getter和setter的话也可以通过这种方式实现class Model {  
    foo = &#39;&#39;
    getFoo() {        return this.foo
    }
    setFoo(value) {        this.foo = value        this.search()
    }
    search() {        // ...
    }
}

Mobx and Vue use this method to implement responsiveness. Of course, we can also use Proxy if compatibility is not considered.

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

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

Mobx will collect the values ​​that time depends on at runtime, and use these Recalculating the value of time when the value changes (triggering the setter) is obviously much more convenient and efficient than the EventEmitter method, and is more intuitive than the Redux middleware.

But there is also a disadvantage here. The computed attribute based on getter can only describe the situation of y = f(x). However, in many cases in reality, f is an asynchronous function, so it will become y = await f( x), getter cannot describe this situation.

For this situation, we can use the autorun provided by Mobx to achieve:

class Model {  
    @observable keyword = &#39;&#39;
    @observable searchResult = []    constructor() {
        autorun(() => {            // ajax ...
        })
    }
}

Since the runtime dependency collection process is completely implicit, a problem often encountered here is the collection Unexpected dependency:

class Model {  
    @observable loading = false
    @observable keyword = &#39;&#39;
    @observable searchResult = []    constructor() {
        autorun(() => {            if (this.loading) {                return
            }            // ajax ...
        })
    }
}

Obviously the loading here should not be collected by the searched autorun. In order to deal with this problem, there will be some extra code, and the extra code will easily bring the opportunity for mistakes. Alternatively, we can also manually specify the required fields, but this method requires some additional operations:

class Model {  
    @observable loading = false
    @observable keyword = &#39;&#39;
    @observable searchResult = []
    disposers = []
    fetch = () => {        // ...
    }
    dispose() {        this.disposers.forEach(disposer => disposer())
    }    constructor() {        this.disposers.push(
            observe(this, &#39;loading&#39;, this.fetch),
            observe(this, &#39;keyword&#39;, this.fetch)
        )
    }
}class FooComponent extends Component {  
    this.mode = new Model()
    componentWillUnmount() {        this.state.model.dispose()
    }    // ...}

And when we need to describe the timeline, Mobx is somewhat unable to do what it does. For example, you need to delay the search for 5 seconds.

Related recommendations:

Puzzle responsive front-end framework version of responsive backend officially released_html/css_WEB-ITnose

Use a very simple responsive front-end development framework_html/css_WEB-ITnose


The above is the detailed content of Detailed introduction to front-end responsive programming solutions and their shortcomings (with code). For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn