用RxJS和react开发mac地址输入框

  • 项目简介
  • RxJS简介
  • 项目结构
  • 项目详解
  • 170624更新
目录

项目简介

本次使用了RxJS和react开发了一个mac地址输入框,主要实现的功能有限制输入符合条件的字符1-9,a-f,并每隔两位可以自动添加用于分割的冒号。项目屏蔽了react的事件处理,同时使用setSelectionRange来手动控制光标。可以查看项目的demo,项目地址

RxJS简介

RxJS 是 Reactive Extensions 在 JavaScript 上的实现,具体来说是一系列工具库,包括事件处理,函数节流,延时等函数,RxJS应用了’流‘的思想,同时具有事件和时间的概念。RxJS也可以用于处理异步流程,比起Promise具有可取消和可延迟,重试等优点。Promise vs Observable
RxJS中有两个比较重要的概念,分别是Observable和observer。Observable可以使用create,of,from,fromEvent等方法来产生流,而Observer可以对流进行观察。最后两者通过subscribe来结合,例子如下:

 var Observable = Rx.Observable.create(observer => {     observer.next(2);     observer.complete();     return  () => console.log('disposed'); });  var Observer = Rx.Observer.create(     x => console.log('Next:', x),     err => console.log('Error:', err),     () => console.log('Completed') );  var subscription = Observable.subscribe(Observer); 

来自构建流式应用—RxJS详解

更多关于RxJS,可以阅读Introduction | RxJS - Javascript library for functional reactive programming.

项目结构

     // 监听事件,发起流和处理流     componentDidMount () {     this.t = ReactDOM.findDOMNode(this.refs.t)     let keydownValue = Rx.Observable.fromEvent(this.t,'keydown').map(e => e.key.toUpperCase())     this.sa = keydownValue.filter(value => value.length === 1 && value.match(/[0-9A-F]/)).subscribe(value => {this.setColon('before');this.insertValue(value); this.setColon();this.setDomValue()})     // 省略类似的部分     }     // 取消订阅     componentWillUnmount()      this.sa.dispose()     // 类似的部分省略     }          // 一些用到的方法,这里省略               // 取消原生的事件监听     render() {       return (         
e.preventDefault()} ref="t"/>
); }

项目详解

首先使用Rx.Observable.fromEvent来监听输入框的按键事件,并获取按键的key值,保存为keydownValue

 let keydownValue = Rx.Observable.fromEvent(this.t,'keydown')  .map(e => e.key.toUpperCase())

接着首先考虑输入字符的情况,在这里,显示筛选出按键符合要求的情况,接着在subscribe中对数据进行处理。在插入新的字符之前和之后,都需要判断是否在前面加上冒号,最后使用setDomValue来让保存在state中的value显示到输入框上。

    this.sa = keydownValue         .filter(value => value.length === 1 && value.match(/[0-9A-F]/))         .subscribe(value => {           this.setColon('before');           this.insertValue(value);            this.setColon();           this.setDomValue()         })

判断是否需要插入冒号的函数setColon,需要排除前面没有字符和周围已经有冒号的情况。

  setColon = type => this.state.value.length &&        (type !== 'before' ? !this.isNearColon() : !this.isLastColon()) &&        !(this.state.value.slice(0, this.state.pos).replace(/:/g, '').length%2) &&        this.insertValue(':') 

插入新字符的函数。在记录的光标位置pos值上插入新的字符,然后改变光标位置。如果在字符末尾有未完成的字符对(即1f:的形式)又在中间插入新的字符串且字符对已经到达六个,则删掉最后一个字符对。

  insertValue = value => {     if (this.state.value.length !== 17) {       this.setState({       ...this.state,       value: this.state.value.slice(0, this.state.pos) +          value + this.state.value.slice(this.state.pos, this.state.value.length)       })       this.setPos(this.state.pos + 1)       if (this.state.value.split(':').length === 7) {         this.setState({         ...this.state,          value: this.state.value.slice(0, this.state.value.lastIndexOf(':'))         })       }   }}

接着是讲解关于删除的流,筛选按键值为'BACKSPACE'的流,执行deleteValue方法和setDomValue

    this.sb = keydownValue.filter(value => value === 'BACKSPACE')     .subscribe(() => {       this.deleteValue()       this.setDomValue()     })

deleteValue,在value和位置都大于零时才执行,如果删除后字符后,新的最后一个字符是冒号,则自动删掉该冒号。

  deleteValue = () => {     if (this.state.value.length && this.state.pos) {       this.setState({       ...this.state,        value: this.state.value.slice(0, this.state.pos - 1) +        this.state.value.slice(this.state.pos, this.state.value.length)       })       this.setPos(this.state.pos - 1)       if (this.isLastColon()) {         this.deleteValue()       }     }   }

接着是订阅了左右方向键移动的流,比较简单,就不详细解释了。

    this.sc = keydownValue         .filter(value => value === 'ARROWLEFT')         .subscribe(() => this.moveLeft())     this.sd = keydownValue         .filter(value => value === 'ARROWRIGHT')         .subscribe(() => this.moveRight())         moveLeft = () => this.state.pos > 0 &&        this.setState({...this.state, pos: this.state.pos - 1})       moveRight = () => this.state.pos !== this.state.value.length &&        this.setState({...this.state, pos: this.state.pos + 1})

最后是让光标跳到pos的处理,setSelectionRange本用于文字的选择,但如果前两个参数为一样的数值,可以达到让光标跳到指定位置的效果。

    this.se = keydownValue.subscribe(() => this.goPos())     goPos = () => this.t.setSelectionRange(this.state.pos, this.state.pos)

170624更新

原本的模式跟react关系较少,因此修改调整了一下,主要的变化是启用了Subject,setStateAsync,在这里先介绍一下。

Rx.Subject

Subject继承于Obserable和Observer,因此同时具有Obserable和Observer两者的方法。通过来自于Observable的multicast方法可以挂载subject,并得到拥有相同执行环境的多路的新的Observable,关于他的订阅实际上是挂载在subject上。最后需要手动connect。 RxJS 核心概念之Subject,30 天精通 RxJS(24): Observable operators - multicast, refCount, publish, share

var source = Rx.Observable.from([1, 2, 3]); var multicasted = source.multicast(new Rx.Subject())  // 通过`subject.subscribe({...})`订阅Subject的Observer: multicasted.subscribe({   next: (v) => console.log('observerA: ' + v) }); multicasted.subscribe({   next: (v) => console.log('observerB: ' + v) });  // 让Subject从数据源订阅开始生效: multicasted.connect();

其实可以用refCount来避免connect,用publish来代替 multicast(new Rx.Subject()),最后用share代替publish 和 refCount,因此代码可以写成

var multicasted = source.share() 
setStateAsync

组件改为受控组件之后,setState中的异步特性展示了出来,setState后的下一步获取setState并不是最新的state,影响了程序的正常使用。
例如之前的新增函数的订阅。后面的inserValue和setColon都是需要利用最新的state来进行判断的。

    this.sa = keydownValue       .filter(value => value.length === 1 && value.match(/[0-9A-F]/))         .subscribe(value => {           this.setColon('before');           this.insertValue(value);            this.setColon();           this.setDomValue()         })

可以在setState的第二个参数中传入回调函数来解决这个问题,于是函数变成了这样,一层又一层的回调,十分不美观

this.sa = keydownValue   .filter(value => value.length === 1 && value.match(/[0-9A-F]/))     .subscribe(value => {       this.setColon('before', () => {         this.insertValue(value, () => {           this.setColon()         })       })     })     

接着在网上找到了setStateAsync的函数,原理就是将setState转换成promise的形式,接着就能愉快的使用async await的语法来修改state了。React中setState同步更新策略

  setStateAsync = state => new Promise(resolve => this.setState(state,resolve))
实际的调整

在componentDidMount中把keydownValue设置为同时具有Observable和Observe的方法的Subject,他一方面可以使用Observer的onNext方法来添加新的数据,另一方面可以继续使用Observable的操作符来对数据进行处理。

this.keydownValue = new Rx.Subject() let multicasted = this.keydownValue.map(e => e.key.toUpperCase()).share() this.sa = multicasted   .filter(value => value.length === 1 && value.match(/[0-9A-F]/))     .subscribe(async value => {     await this.setColon('before')     await this.insertValue(value)     await this.setColon()     this.goPos()   }) //下略    

组件的render函数修改为

  

handleE函数继续禁止默认事件,调用了新设置的Subject(keydownValue)的onNext方法,可以使得绑定在keydownValue上的订阅获得数据

  handleE = e => {e.preventDefault();this.keydownValue.onNext(e)}