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

简介:

上一篇博客提到了几种响应式的方案,以及它们的缺点。本文将介绍Observable以及它的一个实现,以及它在处理响应式时相对于上篇博客中的方案的巨大优势(推荐两篇博客对比阅读)。

Observable是一个集合了观察者模式、迭代器模式和函数式的库,提供了基于事件流的强大的异步处理能力,并且已在Stage 1草案中。本文介绍的RxjsObservable的一个实现,它是ReactiveX众多语言中的JavaScript版本。

JavaScript中,我们可以使用T | null去处理一个单值,使用Iterator去处理多个值得情况,使用Promise处理异步的单个值,而Observable则填补了缺失的“异步多个值”。

单个值 多个值
同步 T | null Iterator<T>
异步 Promise<T> Observable<T>

使用Rxjs

上文提到使用Event Emitter做响应式处理,在Rxjs中稍有些不同:



/*

const change$ = new Subject();

<Input change$={change$} />

<Search change$={change$} />

*/

class Input extends Component {
  state = {
    value: ''
  };

  onChange = e => {
    this.props.change$.next(e.target.value);
  };

  componentDidMount() {
    this.subscription = this.props.change$.subscribe(value => {
      this.setState({
        value
      });
    });
  }

  componentWillUnmount() {
    this.subscription.ubsubscribe();
  }

  render() {
    const { value } = this.state;

    return <input value={value} onChange={this.onChange} />;
  }
}

class Search extends Component {
  // ...

  componentDidMount() {
    this.subscription = this.props.change$.subscribe(value => {
      ajax(/* ... */).then(list =>
        this.setState({
          list
        })
      );
    });
  }

  componentWillUnmount() {
    this.subscription.ubsubscribe();
  }

  render() {
    const { list } = this.state;

    return <ul>{list.map(item => <li key={item.id}>{item.value}</li>)}</ul>;
  }
}

在这里,我们虽然也需要手动释放对事件的订阅,但是得益于 Rxjs 的设计,我们不需要像 Event Emitter 那样去存下回调函数的实例,用于释放订阅,因此我们很容易就可以通过高阶组件解决这个问题。例如:



const withObservables = observables => ChildComponent => {
  return class extends Component {
    constructor(props) {
      super(props);
      this.subscriptions = {};
      this.state = {};
      Object.keys(observables).forEach(key => {
        this.subscriptions[key] = observables[key].subscribe(value => {
          this.setState({
            [key]: value
          });
        });
      });
    }

    onNext = (key, value) => {
      observables[key].next(value);
    };

    componentWillUnmount() {
      Object.keys(this.subscriptions).forEach(key => {
        this.subscriptions[key].unsubscribe();
      });
    }

    render() {
      return (
        <ChildComponent {...this.props} {...this.state} onNext={this.onNext} />
      );
    }
  };
};

这样在需要聚合多个数据源时,也不会像 Event Emitter 那样手动释放资源造成麻烦。同时,在 Rxjs 中我们还有专用于聚合数据源的方法:


Observable.combineLatest(foo$, bar$)
  .pipe(
      // ...
  );

显然相对于 Event Emitter的方式十分高效,同时它相对于 Mobx也有巨大的优势。在 Mobx中,我们提到需要聚合多个数据源的时候,采用 autoRun的方式容易收集到不必要的依赖,使用 observe则不够高效。在 Rxjs中,显然不会有这些问题, combineLatest可以以很简练的方式声明需要聚合的数据源,同时,得益于 Rxjs设计,我们不需要像 Mobx一个一个去调用 observe返回的析构,只需要处理每一个 subscribe返回的 subscription


class Foo extends Component {
  constructor(props) {
    super(props);
    this.subscription = Observable.combineLatest(foo$, bar$)
      .pipe(
        // ...
      )
      .subscribe(() => {
        // ...
      });
  }

  componentWillUnmount() {
    this.subscription.unsubscribe();
  }
}

异步处理

Rxjs使用操作符去描述各种行为,每一个操作符会返回一个新的Observable,我们可以对它进行后续的操作。例如,使用map操作符就可以实现对数据的转换:


foo$.map(event => event.target.value);

Rxjs 5.5 之后所有的 Observable 上都引入了一个 pipe 方法,接收若干个操作符, pipe 方法会返回一个 Observable 。因此,我们可以很容易配合 tree shaking 实现对操作符的按需引入,而不是把整个 Rxjs 引入进来:



import { map } from 'rxjs/operators';

foo$.pipe(map(event => event.target.value));

推荐使用这种写法。

在讨论面向对象的响应式的响应式中,我们提到对于异步的问题,面向对象的方式不好处理。在Observable中我们可以通过switchMap操作符处理异步问题,一个异步搜索看起来会是这样:


input$.pipe(switchMap(keyword => Observable.ajax(/* ... */)));

在处理异步单值时,我们可以使用 Promise ,而 Observable 用于处理异步多个值,我们可以很容易把一个 Promise 转成一个 Observable ,从而复用已有的异步代码:



input$.pipe(switchMap(keyword => fromPromise(search(/* ... */))));

switchMap 接受一个返回 Observable 的函数作为参数,下游的流就会切到这个返回的 Observable 。 而要聚合多个数据源并做异步处理时:


combineLatest(foo$, bar$).pipe(
  switchMap(keyword => fromPromise(someAsyncOperation(/* ... */)))
);

同时,由于标准制定的Promise是没有cancel方法的,有时候我们要取消异步方法的时候就有些麻烦(主要是为了解决一些并发安全问题)。switchMap当上游有新值到来时,会忽略结束已有未完成的Observable然后调用函数返回一个新的Observable,我们只使用一个函数就解决了并发安全问题。当然,我们可以根据实际需要选用switchMapmergeMapconcatMapexhaustMap等。

而对于时间轴的操作,Rxjs也有巨大优势。上篇博客中提到当我们需要延时 5 秒做操作时,无论是Event Emitter还是面向对象的方式都力不从心,而在Rxjs中我们只需要一个delay操作符即可解决问题:


input$.pipe(
  delay(5000) // 下游会在input$值到来后5秒才接到数据
);

用 Rxjs 处理数据

在实际开发过程中,事件不能解决所有问题,我们往往会需要存储数据,而Observable被设计成用于处理事件,因此它有很多符合事件直觉的设计。

Observable被设计为懒(lazy)的,当当没有订阅者时,一个流不会执行。对于事件而言,没有事件的消费者那么不执行也不会有问题。而在 GUI 中,订阅者可能是View



class View extends Component {
  state = {
    input: ''
  };

  componentDidMount() {
    this.subscription = input$.subscribe(input => {
      this.setState({
        input
      });
    });
  }

  componentWillUnmount() {
    this.subscription.unsubscribe();
  }

  render() {
    // ...
  }
}

由于这个View可能不存在,例如路由被切走了,那么我们的事件源就没有了订阅者,他就不会运行。但是我们希望在路由被且走后,后台的数据依然会继续。

对于事件而言,在事件发生之后的订阅者不会受到订阅之前的逻辑。例如在EventEmitter中:


eventEmitter.emit('hello', 1);
// ...
eventEmitter.on('hello', function listener() {});

由于listener是在hello事件发生后在监听的,不会收到值为1的事件。但是这在处理数据的时候会造成麻烦,我们的数据在View被卸载(例如路由切走)后丢失。

同时,由于Observable没有提供直接取到内部状态的方法,当我们使用Observable处理数据时,我们不方便随时拿到数据。那有办法解决这个问题,从而使Observable强大抽象能力去赋能数据层呢?

回到ReduxRedux的事件(Action)其实是一个事件流,那么我们就可以很自然地把Redux的事件流融入到Rxjs流中:


() => next => {
  const action$ = new Subject();

  return action => {
    action$.next(action);
    // ...
  };
};

通过这样的封装, redux-observable 就能让我们把 Observable 强大的事件描述和处理能力和 Redux 结合。我们可以非常方便地根据 Action 去处理副作用:



action$.pipe(
  ofType('ACTION_1'),
  switchMap(() => {
    // ...
  }),
  map(res => ({
    type: 'ACTION_2',
    payload: res
  }))
);

action$.pipe(
  ofType('ACTION_3'),
  mergeMap(() => {
    // ...
  }),
  map(res => ({
    type: 'ACTION_4',
    payload: res
  }))
);

Redux Observable使我们可以结合 ReduxObservable。在这里, Action被视作一个流, ofType相当于 filter(action => action.type === 'SOME_ACTION'),从而得到需要监听的 Action,得益于 Redux的设计,我们可以通过监听 Action去完成副作用的处理或者监听数据变化。最后这个流返回一个新的 Action流, Redux Observable会把这个新的 Action流中的 Action dispatch出去。由此,我们在使用 Redux存储数据的基础上获得了 Rxjs对异步事件的强大处理能力。


原文发布时间为:2018年06月25日
原文作者:有赞前端

本文来源: 掘金 如需转载请联系原作者




相关文章
|
6月前
|
编解码 前端开发 开发者
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
【4月更文挑战第30天】Flutter框架助力移动应用实现响应式设计与自适应布局,通过层次化布局系统和`Widget`树管理,结合`BoxConstraints`定义尺寸范围,实现自适应。利用`MediaQuery`获取设备信息,调整布局以适应不同屏幕。`FractionallySizedBox`按比例设定尺寸,`LayoutBuilder`动态计算布局。借助这些工具,开发者能创建跨屏幕尺寸、方向兼容的应用,提升用户体验。
150 0
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
|
6月前
|
编解码 前端开发 开发者
现代前端开发中的响应式设计原理与实践
传统的网页设计通过固定的布局方式难以适应不同设备的屏幕尺寸,而响应式设计则能够使网页在各种终端上都能良好呈现。本文将深入探讨现代前端开发中响应式设计的原理和实践,帮助开发者更好地理解和应用响应式设计技术。
|
6月前
|
前端开发 UED
现代前端开发中的响应式设计与移动优先策略
随着移动设备的普及和多样化,现代前端开发越来越注重响应式设计和移动优先策略。本文将介绍响应式设计的概念、原则和实践,以及在移动优先策略下如何构建适配各种设备的前端界面。
|
6月前
|
编解码 前端开发 JavaScript
现代前端开发中的响应式设计与跨平台兼容性
在当今数字化时代,前端开发已成为互联网行业的重要组成部分。本文将介绍现代前端开发中的响应式设计和跨平台兼容性,探讨其在不同设备上的应用以及解决方案,以期帮助开发者更好地应对多样化的用户需求。
|
5月前
|
前端开发 UED 开发者
现代前端开发中的响应式设计原理与实践
本文探讨了现代前端开发中响应式设计的重要性及其实现原理。通过分析媒体查询、弹性网格布局以及视口单位等技术手段,揭示了如何通过这些工具实现页面在不同设备上的优雅适配。最后,结合实际案例展示了响应式设计在提升用户体验和网站性能方面的应用。
|
25天前
|
前端开发 容器
前端技术分享:利用CSS Grid布局实现响应式设计
【10月更文挑战第1天】前端技术分享:利用CSS Grid布局实现响应式设计
|
25天前
|
前端开发 JavaScript Go
前端开发趋势:从响应式设计到Web组件的探索
【10月更文挑战第1天】前端开发趋势:从响应式设计到Web组件的探索
29 3
|
4月前
|
前端开发 JavaScript 开发者
现代前端开发中的响应式设计实践与挑战
随着移动设备的普及,现代前端开发越来越注重响应式设计。本文探讨了响应式设计的实践方法和面临的挑战,从布局策略到性能优化,帮助开发者更好地应对多设备环境下的需求。
|
4月前
|
编解码 前端开发 JavaScript
前端框架与库 - Bootstrap响应式设计
【7月更文挑战第19天】Bootstrap是流行的前端框架,以其响应式设计和组件库闻名。响应式设计确保网站在不同设备上显示良好。关键包括:**网格系统**,基于12列布局,适应屏幕尺寸;和**媒体查询**,用于根据设备特性应用样式。开发者常遇到的问题有:网格列超过12、忽视断点和自定义样式冲突。解决方法涉及正确使用断点类、测试多设备及清晰注释代码。借助官方文档和实践,可有效掌握响应式设计。
76 1
|
3月前
|
Web App开发 编解码 移动开发
现代前端开发中的响应式设计与跨平台兼容性
在当今互联网应用程序的快速发展中,响应式设计和跨平台兼容性已经成为前端开发中至关重要的技术策略。本文将探讨响应式设计的基本原则和实现方法,以及如何通过最新的前端技术实现在不同设备和平台上的一致性用户体验。