2

项目简介

本次使用了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 (
        <div className="App">
          <input type="text" onKeyDown={e => e.preventDefault()}  ref="t"/>
        </div>
      );
    }  
    

项目详解

首先使用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 核心概念之Subject30 天精通 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函数修改为

  <div className="App">  
    <input type="text" onKeyDown={this.handleE} value={this.state.value} ref="t"/>
  </div>

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

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

hpoenixf
534 声望95 粉丝

[链接]