如果在React浏览器项目中有这样一个需求:在一个数据较为敏感的页面,需要监听用户的鼠标点击,如果超过30秒用户没有点击操作则跳回登陆页。再进入页面需要重新登陆。如果30秒内用户点击了鼠标则重新开始计时。 如果在单纯的React体系内解决这个问题呢,当然是可以的,但是需要将处理代码分散在不同的生命周期,点击事件处理函数中去操作定时器。还要注意闭包的问题。
import React, { useState, useEffect, useCallback, useRef } from 'react';
var interval = null;
function App() {
const [tick, setTick] = useState(0);
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = () => {
console.log(tick);
setTick(tick + 1);
};
}, [tick]);
useEffect(() => {
function countUp() {
savedCallback.current();
}
if (interval === null) interval = setInterval(countUp, 1000);
}, []);
useEffect(() => {
if (tick === 30) {
clearInterval(interval);
window.location.href = 'http://***';
}
}, [tick]);
const handlerClick = useCallback(() => {
setTick(0);
}, []);
return (
<div className="App" onClick={handlerClick}>
页面点击监听
</div>
);
}
export default App;
有没有更优雅的处理方案呢?其实业内有一套成熟的基于函数式编程的异步解决方案:Rx.JS。 上面的需求如果用Rx.JS来解决代码如下:
useEffect(() => {
console.log('rx.js');
interval(1000).pipe(takeUntil(fromEvent(document.body, 'click')), repeat())
.subscribe(v => v === 30 && window.location.href='http://***', e => console.error(e), () => { console.log('complete') });
}, [])
是不是优雅了很多。 下面开始介绍一些Rx.JS。当前网络上也有一些Rx.JS的教程,但是一方面它们依赖的Rx.JS的版本有点老,另一方面本文想将Rx.JS和React结合起来。 由于JavaScript原来的单线程的特性(现在有了webwork),导致Javascript里面充斥着大量的异步操作需要处理。浏览器端常见的需要处理异步的情况如下。
- DOMEvent
- Ajax
- Fetch
- Timer
Js原生也不断提供新的异步解决方案。从最开始的回调函数,到Promise,generator,再到async,await。原生处理异步的方案再不断简化和进步。但是处理起复杂的异步情况还是稍微有点麻烦。比如上面一开始的例子。更简单一点再比如我们在React给document里面添加一个点击的eventLisener并且监听一次就移除的代码是这样的
import React, { useEffect } from "react";
import "./App.css";
import { fromEvent } from "rxjs";
import { take } from "rxjs/operators";
function App() {
const handlerClick = () => {
console.log("click document");
document.body.removeEventListener("click", handlerClick);
};
useEffect(() => {
document.body.addEventListener("click", handlerClick);
}, []);
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
export default App;
稍微有点麻烦,我们需要在React生命周期和自定义的事件回调函数中分别去处理异步。
针对复杂的异步处理。比起完全自己来处理,其实Rx.js是一种更好的解决方案。
其实Rx.js还有很多兄弟姐妹,像rxJava,rxPy等等。可以看出Rx是一种响应式的处理异步的解决方案。
那上面的代码如果我们用Rx.js来简化如下
//先安装Rx.JS npm install rxjs
import React, { useState, useEffect } from 'react';
import './App.css';
import { fromEvent} from 'rxjs';
import { take } from 'rxjs/operators';
function App() {
useEffect(() => {
console.log('rx.js');
fromEvent(document.body, 'click').pipe( // 注册监听
take(1) // 只取一次
)
.subscribe(e => console.log(e));
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
export default App;
//注意的是 由于body里面的内容很少,只有鼠标点击h1标签附近才能触发事件。
我们不用自己在不同的生命周期和自定义事件处理函数中去处理异步。通过函数式编程的链式调用就完成了和上面代码同样的功能。
Rx.JS里面有几个重要的概念:函数式编程,Observable,subscribe 这些都不是简单的一两句话可以解释清楚的。 函数式编程的概念可以参考:
Observable和subscribe可以参考Rx.JS官网
下面我们通过一个简单的React组件展示一下如何新建一个Rx.JS对象,订阅该对象,并catch error。
import React, { useEffect } from 'react';
import { Observable } from 'rxjs';
function App() {
useEffect(() => {
console.log('rx.js');
const observableInstance = new Observable(subscriber => {
try {
subscriber.next('Jerry');
subscriber.next('Anna');
throw 'some exception';
} catch (e) {
subscriber.error(e);
}
});
console.log('start');
observableInstance.subscribe(v => console.log(v), e => console.error(e));
console.log('end');
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
export default App;
此时Chrome的控制台打印如下:
上面的例子中是通过new Observable的方式创建了Observable对象,但是Rx.JS提供了大量的创建Observable对象的操作符:
- of
- from
- fromEvent
- fromEventPattern
- Empty
- Never
- throwError
- Interval
- timer
针对上面这些操作符,我都写了简单的demo:
-
of 操作符
import React, { useState, useEffect } from 'react';
import { of } from 'rxjs';
function App() {
useEffect(() => {
console.log('rx.js');
of('Jerry','Anna').subscribe(v=>console.log(v));
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
export default App;
-
from 操作符
function App() {
useEffect(() => {
console.log('rx.js');
from(['Jerry','Anna','test']).subscribe(v=>console.log(v));
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
from和of的区别有点类似于call和apply的区别
from接收字符串作为参数
function App() {
useEffect(() => {
console.log('rx.js');
from('Jerry').subscribe(v=>console.log(v));
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
from接收Promise作为参数
function App() {
useEffect(() => {
console.log('rx.js');
from(new Promise((resolve, reject) => {
setTimeout(() => {
resolve('hello, RxJS');
}, 1000);
})).subscribe(v => console.log(v));
}, [])
RxJS 当前的版本fromPromise操作符没有在RxJS官网找到,应该已经被移除。
-
fromEvent 操作符
function App() {
useEffect(() => {
console.log('rx.js');
fromEvent(document.body,'click')
.subscribe(v => console.log(v));
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
chrome控制台输出结果为
-
fromEventPattern 操作符
传入注册监听及移除监听两种方法的定义
import React, { useState, useEffect } from 'react';
import { fromEventPattern } from 'rxjs';
class Producer {
constructor() {
this.listeners = [];
}
addListener(listener) {
if (typeof listener === 'function') {
this.listeners.push(listener)
} else {
throw new Error('listener 必须是 function')
}
}
removeListener(listener) {
this.listeners.splice(this.listeners.indexOf(listener), 1)
}
notify(message) {
this.listeners.forEach(listener => {
listener(message);
})
}
}
function App() {
useEffect(() => {
console.log('rx.js');
const egghead = new Producer();
fromEventPattern(
(handler) => egghead.addListener(handler),
(handler) => egghead.removeListener(handler)
).subscribe(v => console.log(v));
egghead.notify('Hello! Can you hear me?');
}, [])
return (
<div className="App">
<h1>Rx.Js</h1>
</div>
);
}
export default App;
-
Empty 操作符
//直接输出complete
useEffect(() => {
console.log('rx.js');
empty().subscribe(v => console.log(v), e => console.log(e), () => console.log('complete'));
}, [])
-
Never 操作符
useEffect(() => {
console.log('rx.js');
never().subscribe(v => console.log(v), e => console.log(e), () => console.log('complete'));
}, [])
-
throwError 操作符
之前叫throw操作符,目前已经改为throwError操作符
useEffect(() => {
console.log('rx.js');
throwError('error了').subscribe(v => console.log(v), e => console.error(e), () => console.log('complete'));
}, [])
-
Interval 操作符
只有一个参数,表示间隔
useEffect(() => {
console.log('rx.js');
interval(1000).subscribe(v => console.log(v), e => console.error(e), () => console.log('complete'));
}, [])
-
timer 操作符
两个参数,第一个为第一次的等待毫秒,第二个参数为后面的间隔
//第一个1会等1秒发出,后面每隔2秒发出一个递增的值
//如果只传一个参数,只会发出一次 1 ,然后就complete了
useEffect(() => {
console.log('rx.js');
timer(1000,2000).subscribe(v => console.log(v), e => console.error(e), () => console.log('complete'));
}, [])
取消订阅
useEffect(() => {
console.log('rx.js');
const subscription = interval(1000).subscribe(v => console.log(v,typeof v), e => console.error(e), () => console.log('complete'));
setTimeout(()=>{
subscription.unsubscribe();
},7000);
}, [])
看到这里,文章最开始的需求相信大家应该能通过Rx.JS的操作符写出来了。
由于Rx.JS庞大且较为复杂,本文从一个之前遇到的实际需求出发介绍了如何结合使用React&Rx.JS。并重点介绍了Rx.JS中的Observable对象并一一给出了基于React hooks的demo。希望可以给在寻找更优秀的JS异步解决方案的同学一些启发和帮助。