项目简介
本次使用了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,可以阅读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,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,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() }) //下略
<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)}