博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
浅谈前端响应式设计(一)
阅读量:6616 次
发布时间:2019-06-24

本文共 5696 字,大约阅读时间需要 18 分钟。

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

响应式编程(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 (                    )    }}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 (            
    {list.map(item =>
  • {item.value}
  • )}
) }}
这里我们会发现用
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日
原文作者:
有赞前端
本文来源: 
如需转载请联系原作者

你可能感兴趣的文章
ifstream读取文件失败和乱码问题
查看>>
Python信息采集器使用轻量级关系型数据库SQLite
查看>>
zookeeper中的exception的问题
查看>>
final+基本类型导致只编译常量类引起的错误
查看>>
分库分表的几种常见玩法及如何解决跨库查询等问题
查看>>
把GPS经纬度放入两个字符串,写入文件
查看>>
Java操作MongoDB实现CRUD
查看>>
给js文件传参数
查看>>
tomcat web.xml启动加载类
查看>>
Linux 配置SSH信任
查看>>
【九度OJ1352】|【剑指offer41】和为S的两个数字
查看>>
《android-文件大小》
查看>>
HTTPS的工作原理
查看>>
PhoneGap使用PushPlugin插件实现消息推送
查看>>
Boyer-Moore 算法介绍
查看>>
关于Java中的单例模式
查看>>
datepicker
查看>>
基于vCenter/ESXi平台CentOS 6.8系统虚拟机Oracle 12c RAC双节点数据库集群搭建
查看>>
CentOS 7输入startx无法启动图形化界面
查看>>
#51CTO学院四周年# 终于在这里遇到你
查看>>