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

简介:

现实世界有很多是以响应式的方式运作的,例如我们会在收到他人的提问,然后做出响应,给出相应的回答。在开发过程中笔者也应用了大量的响应式设计,积累了一些经验,希望能抛砖引玉。

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



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

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



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

显然,无论是代码的优雅度还是执行效率上,非响应式的方式都不如响应式的设计。

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必须是纯函数,因此要实现响应式的方式只有订阅中或者是在中间件中。

如果通过订阅store的方式,由于Redux不能准确拿到哪一个数据放生了变化,因此只能通过脏检查的方式。例如:



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的组件,会随着组件的挂载和卸载被创建和销毁,如果我们希望这个响应式和组件无关的话就有问题了。

另一种方式就是在中间件中监听数据变化。得益于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处理异步事件的实践。



原文发布时间为:2018年06月25日
原文作者: 有赞前端
本文来源:  掘金  如需转载请联系原作者



相关文章
|
2月前
|
前端开发 UED
现代前端开发中的响应式设计与移动优先策略
随着移动设备的普及和多样化,现代前端开发越来越注重响应式设计和移动优先策略。本文将介绍响应式设计的概念、原则和实践,以及在移动优先策略下如何构建适配各种设备的前端界面。
|
2月前
|
编解码 前端开发 JavaScript
现代前端开发中的响应式设计与跨平台兼容性
在当今数字化时代,前端开发已成为互联网行业的重要组成部分。本文将介绍现代前端开发中的响应式设计和跨平台兼容性,探讨其在不同设备上的应用以及解决方案,以期帮助开发者更好地应对多样化的用户需求。
|
2月前
|
编解码 移动开发 前端开发
|
2月前
|
编解码 前端开发 UED
前端开发中的响应式设计原则与技巧
【2月更文挑战第10天】在当今数字化时代,响应式设计已经成为前端开发的重要组成部分。本文将介绍响应式设计的基本原则和一些实用技巧,帮助开发人员创建适应不同设备和屏幕尺寸的网页界面。从媒体查询到流体布局,从移动优先到图像优化,我们将探索如何以用户为中心,提供优质的用户体验。
53 1
|
3月前
|
编解码 前端开发 UED
现代化前端开发中的响应式设计技术
在现代化前端开发中,响应式设计技术扮演着至关重要的角色。本文将介绍响应式设计的概念及其在前端开发中的应用,涵盖了媒体查询、弹性布局、自适应图片等方面的技术。通过学习这些技术,开发人员可以为不同设备和屏幕尺寸提供一致而优雅的用户体验。
|
3月前
|
编解码 前端开发 UED
探索现代前端开发中的响应式设计技术
本文将介绍现代前端开发中的响应式设计技术,包括媒体查询、弹性布局和视口单位等。我们将深入探讨这些技术的原理和应用,以及它们在不同设备上实现自适应界面的重要性。通过学习本文,读者将能够更好地理解和运用响应式设计技术,提升网页在各种设备上的用户体验。
19 3
|
3月前
|
编解码 前端开发 UED
Web前端开发中的响应式设计技巧
【2月更文挑战第6天】在当今互联网快速发展的时代,网站的响应式设计已经成为Web前端开发的重要一环。本文将介绍一些实用的响应式设计技巧,帮助开发者更好地应对不同屏幕尺寸和设备的挑战,提升用户体验。
84 1
|
2月前
|
编解码 前端开发 UED
现代前端开发中的响应式设计原则与实践
在当今移动互联网时代,响应式设计已成为现代前端开发的重要组成部分。本文将介绍响应式设计的基本原则及实践方法,包括弹性网格布局、媒体查询和流式布局等技术,以及在实际项目中的应用经验和常见挑战。
|
2月前
|
编解码 前端开发 UED
现代前端开发中的响应式设计原则与实践
在当今移动互联网时代,响应式设计已成为现代前端开发的必备技能。本文将介绍响应式设计的基本原则和实践方法,通过案例分析展示其在不同设备上的应用,帮助读者更好地理解和运用响应式设计。
|
2月前
|
编解码 前端开发 开发者
前端开发中的响应式设计与移动优先原则
在当今移动互联网时代,用户对网页的访问设备多样化,因此前端开发中的响应式设计和移动优先原则显得尤为重要。本文将探讨响应式设计的概念、实现方法以及移动优先原则在前端开发中的应用,旨在帮助开发者更好地适应多设备访问环境,提升用户体验。