浅谈前端响应式设计(一)

现实世界有不少是以响应式的方式运做的,例如咱们会在收到他人的提问,而后作出响应,给出相应的回答。在开发过程当中我也应用了大量的响应式设计,积累了一些经验,但愿能抛砖引玉。react

响应式编程(Reactive Programming)和普通的编程思路的主要区别在于,响应式以推(push)的方式运做,而非响应式的编程思路以拉(pull)的方式运做。例如,事件就是一个很常见的响应式编程,咱们一般会这么作:ajax

button.on('click', () => {
    // ...
})
复制代码

而非响应式方式下,就会变成这样:编程

while (true) {
    if (button.clicked) {
        // ...
    }
}
复制代码

显然,不管在是代码的优雅度仍是执行效率上,非响应式的方式都不如响应式的设计。redux

Event Emitter

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采用了一个事件流的方式实现响应式,在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引入了gettersetter,咱们能够经过gettersetter实现一种响应式。

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() {
        // ...
    }
}
复制代码

MobxVue就使用了这样的方式实现响应式。固然,若是不考虑兼容性的话咱们还能够使用Proxy

当咱们须要响应若干个值而后获得一个新值的话,在Mobx中咱们能够这么作:

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

Mobx会在运行时收集time依赖了哪些值,并在这些值发生改变(触发setter)的时候从新计算time的值,显然要比EventEmitter的作法方便高效得多,相对Reduxmiddleware更直观。

可是这里也有一个缺点,基于gettercomputed属性只能描述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处理异步事件的实践。

相关文章
相关标签/搜索