我搜集了网络和本身实践中的一些案例,让你们感觉一下rxjs处理异步时的优点。javascript
本文主要目的:php
一、让一些同窗对rxjs对稍微复杂异步的简洁处理感兴趣(不须要懂api,仅仅感觉rxjs的优点)前端
二、让一些缺少实战案例的同窗练手(网上多讲api的,跟业务相关的案例较少,这跟rxjs自己流行程度并不高有关)java
三、本身初步学习后的总结web
题目以下:面试
实现一个批量请求函数 multiRequest(urls, maxNum),要求以下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,能够增长新的请求
• 全部请求完成后,结果按照 urls 里面的顺序依次打出
复制代码
你能够先想一想若是不用rxjs,你会怎么作,咱们先看用rxjs有多简单ajax
// 假设这是你的http请求函数
function httpGet(url) {
return new Promise(resolve => setTimeout(() => resolve(`Result: ${url}`), 2000));
}
复制代码
使用rxjs只须要1行代码就能够解决这个面试题,后面会写一版不用rxjs,能够看看promise实现有多么麻烦。编程
const array = [
'https://httpbin.org/ip',
'https://httpbin.org/user-agent',
'https://httpbin.org/delay/3',
];
// mergeMap是专门用来处理并发处理的rxjs操做符
// mergeMap第二个参数2的意思是,from(array)每次并发量是2,只有promise执行结束才接着取array里面的数据
// mergeMap第一个参数httpGet的意思是每次并发,从from(array)中取的数据如何包装,这里是做为httpGet的参数
const source = from(array).pipe(mergeMap(httpGet, 2)).subscribe(val => console.log(val));
复制代码
在线代码预览:stackblitz.com/edit/rxjs-q… (注意控制台打印顺序)json
如下是promise的版本,代码多并且是面向过程的面条代码(若是不用rxjs的化,通常场景建议使用ramda库,用“流”或者函数组合的方式来编写函数,让你的功能模块远离面条代码(面条代码 = 难以维护的面向过程的代码)),文章最后闲聊会讲业务不复杂的场景,怎么使用ramda。redux
如下是用promise解决上述面试题的思路,能够看到大量的临时变量,while函数,if语句,让代码变得难以维护(并非拒绝这种代码,毕竟优雅的接口后面极可能是“龌龊的实现”),但若是有工具帮助你直接使用优雅的接口,下降了复杂度,何乐而不用呢
function multiRequest(urls = [], maxNum) {
// 请求总数量
const len = urls.length;
// 根据请求数量建立一个数组来保存请求的结果
const result = new Array(len).fill(false);
// 当前完成的数量
let count = 0;
return new Promise((resolve, reject) => {
// 请求maxNum个
while (count < maxNum) {
next();
}
function next() {
let current = count++;
// 处理边界条件
if (current >= len) {
// 请求所有完成就将promise置为成功状态, 而后将result做为promise值返回
!result.includes(false) && resolve(result);
return;
}
const url = urls[current];
console.log(`开始 ${current}`, new Date().toLocaleString());
fetch(url)
.then((res) => {
// 保存请求结果
result[current] = res;
console.log(`完成 ${current}`, new Date().toLocaleString());
// 请求没有所有完成, 就递归
if (current < len) {
next();
}
})
.catch((err) => {
console.log(`结束 ${current}`, new Date().toLocaleString());
result[current] = err;
// 请求没有所有完成, 就递归
if (current < len) {
next();
}
});
}
});
}
复制代码
咱们再来一个面试题,这是我本身面腾讯的时候本身遇到的关于ajax请求并发的问题,当时刚转前端回答的不是很好。题目以下:
再进一步说明问题
按钮A按了以后,ajax请求的数据显示在input type=text框里,B按钮也是。
问题就是若是先按A,此时ajax发出去了,可是数据还没返回来, 咱们等不及了,立刻按B按钮,结果此时A按钮请求的数据先回来,这就尴尬了,按的B按钮,结果先显示A按钮返回的数据,怎么解决?
这个问题能够在在A按钮按了以后,再按B按钮的时候,取消a按钮发出的请求,这个ajax和fetch都是有方法实现的,ajax原生自带cancel方法,fetch的话要本身写一下,大概思路以下(如何取消fetch)
function abortableFetch(request, opts) {
const controller = new AbortController();
const signal = controller.signal;
return {
abort: () => controller.abort(),
ready: fetch(request, { ...opts, signal })
};
}
复制代码
别看上面封装的挺不错的,可是用起来仍是有点麻烦,并且耦合性有点高,由于我要在B按钮的onClick事件里面去调用A按钮的abort方法。
好了,咱们基于rxjs来写一个通用的处理方案(要说函数间的解耦,发布订阅模式有点万能的感受,rxjs的new Subject也是同样的思想)
import { Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
// 假设这是你的http请求函数
function httpGet(url: any): any {
return new Promise(resolve =>
setTimeout(() => resolve(`Result: ${url}`), 2000)
);
}
class abortableFetch {
search: Subject<any>;
constructor() {
this.search = new Subject();
this.init();
}
init() {
this.search
.pipe((switchMap as any)((value: any): any => httpGet(value)))
.subscribe(val => console.log(val));
}
trigger(value) {
this.search.next(value);
}
}
// 使用方式,很是简单,就一个trigger方法就能够了
const switchFetch = new abortableFetch();
switchFetch.trigger(123);
setTimeout(() => {
switchFetch.trigger(456);
}, 1000);
复制代码
请注意此案例控制台输出的是456而不是123,由于456后输出把以前的123覆盖了,至关于取消了以前的请求
在线预览此案例: stackblitz.com/edit/rxjs-z…
好了,上面两个例子能够看出rxjs最大的优势:
一、函数式编程在写一些小功能的时候,解耦很是简单,自然知足高内聚、低耦合
二、rxjs在处理异步(好比网络IO和UI交互)时,写一些小功能时,代码量较少,语义性很强
可是问题也很突出,就是掌握好rxjs,真的不容易,都不说rxjs了,用ramda库或函数式变成的库的前端在我经历里都不多。
接下来,下面就是基于rxjs的一些案例。
bufferTime: 好比你写一个基于 websocket 的在线聊天室,不可能每次 ws 收到新消息,都马上渲染出来,这样在不少人同时说话的时候,通常会有渲染性能问题。。
因此你须要收集一段时间的消息,而后把它们一块儿渲染出来,例如每一秒批量渲染一次。用原生 JS 写的话,你须要维护一个队列池,和一个定时器,收到消息,先放进队列池,而后定时器负责把消息渲染出来,相似:
let messagePool = []
ws.on('message', (message) => {
messagePool.push(message)
})
setInterval(() => {
render(messagePool)
messagePool = []
}, 1000)
复制代码
这里已是最简化的代码了,但逻辑依然很破碎,而且还要考虑清理定时器的问题。若是用 RxJS,代码就好看了不少
import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs/operators';
fromEvent(ws, 'message')
.pipe(bufferTime(1000))
.subscribe(messages => render(messages))
复制代码
记录鼠标两秒能点击多少次
fromEvent(document,'click').pipe(
bufferTime(2000),
map(array=>array.length)
).subscribe(count => {
console.log("两秒内点击次数", count);
});
复制代码
bufferCount: 另一个例子,好比咱们在写一个游戏,当用户连续输入"上上下下左右左右BABA"的时候,就弹出隐藏的彩蛋,用原生 JS 的话也是须要维护一个队列,队列中放入最近12次用户的输入。而后每次按键的时候,都识别是否触发了彩蛋。RxJS 的话就简化了不少,主要是少了维护队列的逻辑:
const code = [
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"KeyB",
"KeyA",
"KeyB",
"KeyA"
]
fromEvent(document, 'keyup').pipe(
map(e => e.code),
bufferCount(12, 1)
).subscribe(last12key => {
if (_.isEqual(last12key, code)) {
console.log('隐藏的彩蛋 \(^o^)/~')
}
})
复制代码
固然 RxJS 还能够复杂得多的逻辑,好比要求只有在两秒内连续输入秘籍,才能触发彩蛋,这里该怎么写
import { fromEvent } from 'rxjs';
import { bufferCount, map, auditTime } from 'rxjs/operators';
const code = ['KeyA', 'KeyB', 'KeyA'];
fromEvent(document, 'keyup')
.pipe(
map(e => (e as any).code),
bufferCount(3, 1),
auditTime(2000)
)
.subscribe(last3key => {
if (_.isEqual(last3key, code)) {
console.log('隐藏的彩蛋 \(^o^)/~')
}
});
复制代码
实现内容以下:
一、首先页面上有一個元素(#drag)
二、当鼠标在元素(#drag)上按下左键(mousedown)时,开始监听鼠标移动(mousemove)的位置
三、当鼠标左键释放(mouseup)时,结束监听鼠标的移动
四、当鼠标移动被监听时,跟着修改原件的样式属性
import { of, fromEvent} from 'rxjs';
import { map, concatMap, takeUntil, withLatestFrom } from 'rxjs/operators';
// 样式省略是绝对定位
const dragEle = document.getElementById('drag')
const mouseDown = fromEvent(dragEle, 'mousedown')
const mouseUp = fromEvent(document, 'mouseup')
const mouseMove = fromEvent(document, 'mousemove')
mouseDown.pipe(
concatMap(e => mouseMove.pipe(takeUntil(mouseUp))),
withLatestFrom(mouseDown, (move: MouseEvent, down: MouseEvent) => {
return {
x: move.clientX - down.offsetX,
y: move.clientY - down.offsetY
}
})
).subscribe(pos => {
dragEle.style.top = pos.y + 'px';
dragEle.style.left = pos.x + 'px';
})
复制代码
在线预览:stackblitz.com/edit/rxjs-s…
实现内容以下:
一、准备 input#search 以及 ul#suggest-list 的 HTML 与 CSS
二、在 input#search 输入文字时,等待 100 毫秒后若无输入,就发送 HTTP Request
三、当 Response 还没回来时,使用者又输入了下一哥文字就舍弃前一次的,并再发送一次新的 Request
四、接受到 Response 以后显示下拉选项
五、鼠标左键选中对应的下拉响,取代 input#search 的文字
import { fromEvent } from "rxjs";
import { map, debounceTime, switchMap } from "rxjs/operators";
const url = 'https://zh.wikipedia.org/w/api.php?action=opensearch&format=json&limit=5&origin=*';
const getSuggestList = (keyword) => fetch(url + '&search=' + keyword, { method: 'GET', mode: 'cors' })
.then(res => res.json())
const searchInput = document.getElementById('search');
const suggestList = document.getElementById('suggest-list');
const keyword = fromEvent(searchInput, 'input');
const selectItem = fromEvent(suggestList, 'click');
const render = (suggestArr = []) => suggestList.innerHTML = suggestArr.map(item => '<li>'+ item +'</li>').join('')
keyword.pipe(
debounceTime(100),
switchMap(
(e: any) => getSuggestList(e.target.value),
(e, res) => res[1]
)
).subscribe(list => render(list))
selectItem.pipe(
map(e => e.target.innerText)
).subscribe(text => {
searchInput.value = text;
render();
})
复制代码
在线预览:stackblitz.com/edit/rxjs-x…
好了,介绍到这里,我我的的感受是rxjs在处理网络层和ui层的逻辑时,在某些特定场景会很是简单。我在此推荐两个很是好的教程,我看到网上竟然没人推荐这两个rxjs学习的教程(上面有个别案例就是今后来的)。
打通rxjs任督二脉: ithelp.ithome.com.tw/users/20020…
30天精通rxjs: ithelp.ithome.com.tw/articles/10…
文章最后,安利另外一个函数式编程的库ramdajs,rxjs属于最近才学的,还没用到项目中
ramdajs是本身用了大概3个月了,有一些心得,确实写了以后代码的可维护性变的高不少,缘由就是你必须遵照设计模式里的单一职责原则,全部功能能复用的函数都会提取出来(用函数式编程会强行让你养成这个习惯)
附一段我本身项目里ramdajs的代码,完毕。
// 这个函数的意思是,在函数链里面,若是其中一个函数返回是null或者undefined就终止函数链
const pipeWhileNotNil = R.pipeWith((f, res) =>
R.isNil(res) ? res : f(res),
);
pipeWhileNotNil([
// checkData用表单校验用的,若是不经过返回null,这个函数链条就终止,不会往下走
// R__是占位符,被R.curry函数柯里化以后,就能够用R.__充当你函数参数的占位符
// 数组里的参数不用管,是业务上须要自定义的
checkData(R.__, ['sdExchangeRate', 'sdEffectDate']),
// 此函数用来筛选,至关于数组的find方法,format2YYYYMMDD是dayjs用来格式化日期的
R.find(
(v) =>
v?.effectDate === format2YYYYMMDD(sdEffectDate),
),
// pipeP是promise函数流的方法,第一个参数必须是promise函数,后面的函数至关于promise里的then里面的函数
R.pipeP(
// 上一个函数执行结果会传给existEqualEffectDate
// promiseModal是一个弹框组件,询问是否要继续某个操做
async (existEqualEffectDate) => {
if (existEqualEffectDate) {
return await promiseModal({
title: `生效日期已存在,是否直接修改汇率?`,
});
} else {
return await promiseModal({
title: `保存后生效日期不可修改,肯定保存?`,
});
}
},
// 最后根据上一个返回的结果是ture仍是false进行最后的操做,R.pipe是同步函数链方法,dispatch是redux里的dispatch
(isGo) => isGo ? R.pipe(R.tail, saveData(dispatch))(data) : dispatch({
type: 'currencyAndExchange/getExchangePairList',
});
),
])(record);
复制代码