前言
當(dāng)我們開始寫項(xiàng)目的時(shí)候,總會(huì)遇到一些情景凫碌,比如定時(shí)任務(wù)扑毡,ajax請(qǐng)求,要求我們?nèi)フ?qǐng)求大的文件或者圖片這些证鸥,然后我們就發(fā)現(xiàn)僚楞,直接寫代碼,會(huì)出現(xiàn)加載緩慢枉层,白屏這樣的問題泉褐,眾所周知,js是單線程的鸟蜡,所以js任務(wù)也是一個(gè)一個(gè)順序執(zhí)行的膜赃,就是同步執(zhí)行,后一個(gè)任務(wù)必須等待前一個(gè)任務(wù)完成之后才能執(zhí)行揉忘,就如前面所說的跳座,如果前一個(gè)任務(wù)花費(fèi)的時(shí)間很長端铛,就會(huì)造成阻塞,給用戶帶來不好的體驗(yàn)疲眷,我們要解決這些問題禾蚕,當(dāng)我們等待但是又不要阻塞程序,所以就需要異步處理這些耗時(shí)間的任務(wù)狂丝。
1.處理異步的方法
假設(shè)有多個(gè)異步任務(wù)换淆,且任務(wù)有依賴關(guān)系,后一個(gè)任務(wù)必須拿到前一個(gè)任務(wù)的執(zhí)行結(jié)果才可以執(zhí)行
作為剛剛?cè)腴T學(xué)習(xí)的小白几颜,比如我倍试,最先能想到的就是函數(shù)調(diào)用了
(1)回調(diào):容易造成回調(diào)地獄
回調(diào)是實(shí)現(xiàn)異步編程最簡單的方法,我們可以在一個(gè)函數(shù)中調(diào)用其他函數(shù)蛋哭,實(shí)現(xiàn)我們想要的邏輯
getData1(data1 => {
getData2(data1, data2 => {
getData3(data2, data3 => {
getData4(data3, data4 => {
getData5(data4, data5 => {
// 終于取到data5了
})
})
})
})
})
乍一看县习,寫的還可以,也達(dá)到了預(yù)期的效果谆趾,就是看起來不美觀躁愿,而且還形成了回調(diào)地獄,但是如果里面出現(xiàn)很多問題棺妓,需要進(jìn)行異常處理及各種邏輯檢查呢攘已,那這一塊代碼就會(huì)變得非常長炮赦,非常復(fù)雜怜跑,可能下次同事看見了你的代碼,直接暴走吠勘。
為了讓代碼更具備可讀性性芬,Promise隆重登場。
(2)Promise:解決回調(diào)地獄
依舊來解決上面的問題
getData1(data1)
.then(data1 => {
return getData2(data1);
})
.then(data2 => {
return getData3(data2);
})
.then(data3 => {
return getData4(data3);
})
.then(data4 => {
return getData(data4);
})
.catch(err => {
console.log(err);
})
我們直接一路鏈?zhǔn)秸{(diào)用剧防,看起來更清楚明了植锉,但是感覺這樣調(diào)用也不是個(gè)辦法。
你的上級(jí)詢問峭拘,那還能做的更優(yōu)雅嗎俊庇?這可難住了我,喔喔喔鸡挠,但是不要怕辉饱,es5的回調(diào)讓我陷入地獄,但是我爬起來了拣展,es6的Promise讓我得到夸獎(jiǎng)彭沼,es7這不是又出了好方法嗎?讓我們接下來看看es7的async/await吧备埃。
(3)async/await:簡化了邏輯姓惑,但是損失了異步帶來的性能優(yōu)勢(shì)(比如把并行變成串行,增加了時(shí)間開銷)
// 定義一個(gè)執(zhí)行Async函數(shù)方法
async function getSSQ () {
let a = await getData1(data1)
let b = await getData2(a)
let c = await getData3(b)
let d = await getData4(c)
let e = await getData5(d)
console.log(d);
}
確實(shí)好用褐奴,也可以使用try..catch了,啊,這你想肯定能滿足上級(jí)的要求了于毙,又簡單又優(yōu)雅敦冬,小白一眼就看懂,async/await帶著我走向了人生巔峰唯沮。
(4)各個(gè)方法優(yōu)缺點(diǎn)總結(jié)
方法 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
回調(diào) | 簡單邏輯處理很方便 | 邏輯復(fù)雜時(shí)容易造成回調(diào)地獄 |
Promise | 1.狀態(tài)改變就不會(huì)再變匪补,任何時(shí)候都能得到確切的結(jié)果 2.寫法符合思維邏輯 | 1.一旦創(chuàng)建,立即執(zhí)行烂翰,中途無法取消 2.處于pending狀態(tài)時(shí)夯缺,無法得知狀態(tài)3.不設(shè)置回調(diào)函數(shù),內(nèi)部錯(cuò)誤無法反映到外部 |
Async/await | 1. 做到了串行的同步寫法 2.代碼清晰明了 | 1.做不到并行甘耿,除非await不在一個(gè)函數(shù)里面 2.沒有了promise的方法踊兜,比如race() 3.沒有Promise的reject方法,得寫在try...catch中 |
代碼非常簡潔易讀了佳恬,但是學(xué)海無涯捏境,我發(fā)現(xiàn)現(xiàn)在有了一個(gè)新的技術(shù),叫做FRP毁葱,看了一些文章垫言,文章里面一直說有了FRP,就使用流來處理異步倾剿,把異步數(shù)據(jù)看成數(shù)據(jù)流來處理筷频,會(huì)讓事情更簡單
那FRP到底是什么呢?
2.FRP是什么
首先讓我們先使用FRP直接實(shí)現(xiàn)上述的需求
function getSSQ() {
let data1 = 1;
return rxjs.from(getData(data1)).pipe(
rxjs.operators.mergeMap(a =>
rxjs.from(getData(a))
),
rxjs.operators.mergeMap(b =>
rxjs.from(getData(b))
),
rxjs.operators.mergeMap(c =>
rxjs.from(getData(c))
),
rxjs.operators.tap(console.log),
rxjs.operators.mergeMap(d =>
rxjs.from(getData(d))
),
rxjs.operators.tap(e => {
}),
);
}
getSSQ().subscribe({
// next(x) { console.log('got value ' + x); },
// error(err) { console.error('something wrong occurred: ' + err); },
complete() { console.log('done'); }
})
看不懂吧前痘,看不懂沒關(guān)系凛捏,只需要知道from是建立流的,mergeMap()是操作流的芹缔,subscribe是訂閱流的坯癣,最后直接輸出結(jié)果就好了,我們先來了解一下什么是FRP及實(shí)際應(yīng)用最欠,重點(diǎn)是學(xué)習(xí)FRP不同的思維方式
(1)概念
FRP(Functional Reactive Programming),也叫函數(shù)式響應(yīng)式編程
函數(shù)反應(yīng)式編程 = 函數(shù)式編程(Functional programming)+響應(yīng)式編程(Reactive Programming)
如果不知道函數(shù)式編程的朋友示罗,推薦看這個(gè)編程指南,這邊主要講解響應(yīng)式編程
響應(yīng)式編程使用異步數(shù)據(jù)流編程芝硬,即將各種數(shù)據(jù)【包括http請(qǐng)求蚜点、DOM事件等】包裝成流的形式,用操作符對(duì)流進(jìn)行操作吵取,能用同步方式處理異步數(shù)據(jù)
光看這個(gè)概念禽额,我是完全沒法看明白的,所以需要拆開來看
(2)數(shù)據(jù)流是什么?
數(shù)據(jù)流是按時(shí)間排序的即將發(fā)生的事情的序列
舉個(gè)例子脯倒,我們寫代碼時(shí)实辑,會(huì)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換運(yùn)算,比如先轉(zhuǎn)成什么再轉(zhuǎn)成什么再轉(zhuǎn)成什么藻丢,轉(zhuǎn)換這整個(gè)過程相當(dāng)于一個(gè)流著數(shù)據(jù)的管道剪撬,數(shù)據(jù)以流的方式在這個(gè)管道中流通,這些數(shù)據(jù)轉(zhuǎn)換我們會(huì)使用各種方法悠反,相當(dāng)于傳入數(shù)據(jù)作為函數(shù)參數(shù)轉(zhuǎn)換后得到新數(shù)據(jù)結(jié)果残黑,最后的結(jié)果從管道中流出。
流轉(zhuǎn)換的思想為將數(shù)據(jù)事件抽象成管道中流通的流體斋否,轉(zhuǎn)換成新的數(shù)據(jù)事件梨水,這些事件還包含了基本的數(shù)據(jù)值,還可以進(jìn)行相應(yīng)的運(yùn)算茵臭,這種運(yùn)算讓我們不需要花時(shí)間去進(jìn)行事件監(jiān)聽什么的疫诽,我們只需要專注于數(shù)據(jù)的轉(zhuǎn)換,也就是事件的使用,而不是直接操作數(shù)據(jù)旦委。
所以我們?cè)趯W(xué)習(xí)這章內(nèi)容的時(shí)候奇徒,還應(yīng)該學(xué)會(huì)轉(zhuǎn)換思維。
總體思想:什么都可以是流缨硝,變量摩钙,用戶輸入,屬性查辩,高速緩存胖笛,數(shù)據(jù)結(jié)構(gòu)等,將時(shí)間線上的數(shù)據(jù)建模成流
(2)響應(yīng)式是什么宜肉?變化傳遞(跟著變化)
vue就是響應(yīng)式編程匀钧,我們只需要關(guān)注數(shù)據(jù)變化,不需要操作視圖改變谬返,因?yàn)橐晥D會(huì)跟著改變
(3)觀察者模式
是一種設(shè)計(jì)模式,允許定義一種訂閱機(jī)制日杈,可以在對(duì)象事件發(fā)生時(shí)通知多個(gè)觀察的該對(duì)象的其他對(duì)象
比如你花錢了之后銀行會(huì)給你發(fā)消息遣铝,就是觀察者模式,余額是被觀察的對(duì)象莉擒,用戶是觀察者
(4)迭代器模式
游標(biāo)模式酿炸,挨著挨著一步一步運(yùn)行
比如map,set,array都使用了迭代器模式
3.使用案例
前端的FRP的庫:Rxjs【比較多人使用】、Most涨冀,后續(xù)內(nèi)容使用Rxjs
Rxjs中文文檔:https://cn.rx.js.org/
Rxjs英文文檔:https://rxjs.dev/
讓我們學(xué)習(xí)幾個(gè)小案例填硕,來體會(huì)FRP的魅力吧~~
可引入也可以使用npm安裝:
<script src="https://cdn.bootcdn.net/ajax/libs/rxjs/7.3.0/rxjs.umd.js"></script>
RxJS 是一個(gè)庫,它通過使用 observable 序列來編寫異步和基于事件的程序。它提供了一個(gè)核心類型 Observable扁眯,附屬類型 (Observer壮莹、 Schedulers、 Subjects) 和操作符 (map姻檀、filter命满、reduce、every, 等等)绣版,這些數(shù)組操作符可以把異步事件作為集合來處理胶台。
基本概念:
Observable (可觀察對(duì)象): 表示一個(gè)概念,這個(gè)概念是一個(gè)可調(diào)用的未來值或事件的集合杂抽。
Observer (觀察者): 一個(gè)回調(diào)函數(shù)的集合诈唬,它知道如何去監(jiān)聽由 Observable 提供的值。
Subscription (訂閱): 表示 Observable 的執(zhí)行缩麸,主要用于取消 Observable 的執(zhí)行讯榕。
Operators (操作符): 采用函數(shù)式編程風(fēng)格的純函數(shù) (pure function),使用像
map
匙睹、filter
愚屁、concat
、flatMap
等這樣的操作符來處理集合痕檬。Subject (主體): 相當(dāng)于 EventEmitter霎槐,并且是將值或事件多路推送給多個(gè) Observer 的唯一方式。
Schedulers (調(diào)度器): 用來控制并發(fā)并且是中央集權(quán)的調(diào)度員梦谜,允許我們?cè)诎l(fā)生計(jì)算時(shí)進(jìn)行協(xié)調(diào)丘跌,例如
setTimeout
或requestAnimationFrame
或其他。
借用官網(wǎng)第一個(gè)例子入門
注冊(cè)事件監(jiān)聽器的常規(guī)寫法如下
var button = document.querySelector('button');
button.addEventListener('click', () => console.log('Clicked!'));
使用 RxJS 的話唁桩,創(chuàng)建一個(gè) observable 來代替闭树。
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));
(1)實(shí)現(xiàn)計(jì)數(shù)器
a.以前實(shí)現(xiàn)計(jì)數(shù)器:
直接想實(shí)現(xiàn)方法,直接定義全局變量開始寫實(shí)現(xiàn)細(xì)節(jié),點(diǎn)擊則全局變量+1然后打印荒澡,這是我們平時(shí)思考的正常思維
let counter = 0;
buttton.on("click", ()=>{
counter+=1;
console.log(counter);
})
缺點(diǎn):
使用了全局變量:容易被改變值报辱,輸入輸出不確定性,后期維護(hù)困難等
b.有了FRP之后實(shí)現(xiàn)計(jì)數(shù)器:
已知?jiǎng)?chuàng)建流的函數(shù)formEvent
-
已知操作流的函數(shù):pipe单山、map碍现、scan、subscribe
pipe:用于鏈接可觀察的運(yùn)算符
map:類似于Array.prototypr.map()米奸,它把每個(gè)源值傳遞給轉(zhuǎn)化函數(shù)以獲得相應(yīng)的輸出值昼接。
scan:數(shù)組的 reduce 類似。它需要一個(gè)暴露給回調(diào)函數(shù)當(dāng)參數(shù)的初始值悴晰。每次回調(diào)函數(shù)運(yùn)行后的返回值會(huì)作為下次回調(diào)函數(shù)運(yùn)行時(shí)的參數(shù)
subscribe:監(jiān)聽流慢睡,訂閱流
我們先在api中找到了對(duì)應(yīng)的方法,formEvent直接創(chuàng)建一個(gè)流->用pipe進(jìn)行數(shù)據(jù)流的連接(在里面可以寫事件的實(shí)現(xiàn)方法,我們只需要考慮怎么運(yùn)用事件處理漂辐,而不需要直接去操作數(shù)據(jù))->利用map進(jìn)行初始化操作->scan進(jìn)行數(shù)據(jù)相加操作->subscribe()方法訂閱整個(gè)流泪喊,最后輸出
rxjs.fromEvent(document.querySelector('.this'), 'click').pipe( // 連接運(yùn)算符
rxjs.operators.map((_) => 1), // 將原值全變?yōu)?,不用定義全局變量
rxjs.operators.scan((sum, val) => { // 相加
return sum + val;
}, 0)
).subscribe((x) => { console.log('got value ' + x); }); //打印
優(yōu)點(diǎn):
和直接操作數(shù)據(jù)相比者吁,除了創(chuàng)建流之外窘俺,F(xiàn)RP不需要有全局變量,直接可操作
(2)實(shí)現(xiàn)雙擊
例子:如果兩次click之間的間隔時(shí)間小于等于250ms复凳,為一次雙擊瘤泪,否則為兩次單擊,請(qǐng)?jiān)趩螕粲恕㈦p擊時(shí)分別log
以前實(shí)現(xiàn)雙擊:我們會(huì)考慮時(shí)間戳对途,判斷點(diǎn)擊事件的間隔時(shí)間是否小于等于250ms,然后進(jìn)行判斷髓棋,但是會(huì)出現(xiàn)問題实檀,如果連點(diǎn)三次,判斷上就會(huì)出現(xiàn)問題,或者設(shè)置標(biāo)志位按声,但是不管哪種方式實(shí)現(xiàn)膳犹,都會(huì)有些困難
<input type="button" onclick="aa()" ondblclick="bb()" value="點(diǎn)我">
<script language="javascript">
var isdb; //設(shè)置變量
function aa(){
isdb=false; //標(biāo)志位
window.setTimeout(cc, 250) //這里調(diào)用window
function cc(){
if(isdb!=false)return;
console.log("單擊")
}
}
function bb(){
isdb=true;
console.log("雙擊")
}
FRP實(shí)現(xiàn)雙擊:
- 已知操作流的函數(shù):debounceTime、buffer签则、filter
debounceTime:去抖動(dòng)的作用须床,控制發(fā)送頻率操作
buffer:將過往的值收集到一個(gè)數(shù)組中
filter:類似于 Array.prototype.filter(), 它只會(huì)發(fā)出符合標(biāo)準(zhǔn)函數(shù)的值渐裂。
雙擊事件也是操作api豺旬,有直接可以使用的去抖動(dòng)方法debounceTime(),思考流程應(yīng)該是點(diǎn)擊事件創(chuàng)建流->然后這個(gè)流去抖動(dòng)->收集去抖動(dòng)的值->判斷產(chǎn)生的每個(gè)數(shù)組長度,等于2就是雙擊柒凉,同理可得等于1就是單擊
var button = document.querySelector('.this');
var clickStream = rxjs.fromEvent(button, 'click'); //創(chuàng)建流
var doubleClickStream = clickStream.pipe(
rxjs.operators.buffer( // 收集點(diǎn)擊事件到數(shù)組中族阅,
clickStream.pipe(
rxjs.operators.debounceTime(250)
)
),
rxjs.operators.map(function (list) { return list.length; }),//返回?cái)?shù)組長度
rxjs.operators.filter(function (x) { //過濾出雙擊
return x === 2;
})
);
//同理 單擊
var singleClickStream = clickStream.pipe(
rxjs.operators.buffer(
clickStream.pipe(
rxjs.operators.debounceTime(250)
)
),
rxjs.operators.map(function (list) { return list.length; }),
rxjs.operators.filter(function (x) { return x === 1; })
);
// 顯示
singleClickStream.subscribe(function (x) {
document.querySelector('h2').textContent = 'click';
});
doubleClickStream.subscribe(function (x) {
document.querySelector('h2').textContent = '' + x + 'x click';
});
(3)實(shí)現(xiàn)拖動(dòng)
請(qǐng)使用mousedown、mousemove膝捞、mouseup事件來實(shí)現(xiàn)“鼠標(biāo)拖動(dòng)時(shí)坦刀,log:draging”
a.以前實(shí)現(xiàn):js實(shí)現(xiàn)拖拽,以前實(shí)現(xiàn)拖拽绑警,起碼是一兩百行代碼起步求泰,而且邏輯判斷上可能還會(huì)出現(xiàn)問題
b.現(xiàn)在FRP實(shí)現(xiàn):
-
已知操作流的函數(shù):flatMap【現(xiàn)在已變成mergeMap】、takeUntil
flatmap:每個(gè)流進(jìn)行運(yùn)算然后合并輸出
takeUntil:先發(fā)出一個(gè)流的值计盒,直到第二個(gè)流發(fā)出值,就完成
但是當(dāng)我們使用Rxjs實(shí)現(xiàn)的時(shí)候芽丹,代碼實(shí)現(xiàn)就會(huì)變得很少北启,只需要幾行就可以實(shí)現(xiàn)需求,如下所示【具體理解上可能會(huì)有些困難,學(xué)習(xí)具體的推薦從官方中文文檔入手咕村,比較詳細(xì)】
let mousedown = rxjs.fromEvent(document, 'mousedown');
let mousemove = rxjs.fromEvent(document, 'mousemove');
let mouseup = rxjs.fromEvent(document, 'mouseup');
mousedown.pipe(
rxjs.operators.flatMap((_) => {
return mousemove.pipe(rxjs.operators.takeUntil(mouseup))
})
).subscribe(() => {
console.log("draging");
})
4.為什么要使用FRP
從處理異步的方法上场钉,我們發(fā)現(xiàn)async/await并不擅長處理并行需求,雖然也可以處理懈涛,但是耗費(fèi)時(shí)間多些逛万,但是FRP操作符,對(duì)于并行串行都可以很適用
流處理方式的價(jià)值且遠(yuǎn)不止于此批钠,對(duì)于事件處理也非常適用宇植,響應(yīng)式編程的思維方式也是非常有價(jià)值的一點(diǎn)
FRP的特性總結(jié)如下:
-
純凈性 (Purity)
使得 RxJS 強(qiáng)大的正是它使用純函數(shù)來產(chǎn)生值的能力。這意味著你的代碼更不容易出錯(cuò)埋心。
通常你會(huì)創(chuàng)建一個(gè)非純函數(shù)指郁,在這個(gè)函數(shù)之外也使用了共享變量的代碼,這將使得你的應(yīng)用狀態(tài)一團(tuán)糟拷呆。
-
流動(dòng)性 (Flow)
RxJS 提供了一整套操作符來幫助你控制事件如何流經(jīng) observables 闲坎。
5.總結(jié)
(1)優(yōu)點(diǎn)
-
抽象層面更高
FRP以流為單位,封裝了時(shí)間序列和具體的數(shù)據(jù)茬斧,隱藏了“狀態(tài)的同步”腰懂、“異步邏輯的具體實(shí)現(xiàn)”等底層細(xì)節(jié)。
-
和函數(shù)式編程配合使用
能夠使用組合项秉,像管道處理一樣處理各種流绣溜,符合函數(shù)式編程的思維。
提供非阻塞伙狐、異步特性涮毫,便于處理異步情景,但是得是有非常復(fù)雜的異步情景時(shí)才適用贷屎,平時(shí)的簡單異步請(qǐng)求罢防,90%都是可以被async/await還有Promise解決的
避免模式混用,回調(diào)和promise混用唉侄、全局變量和局部變量混用咒吐,最后可能成為無盡的callback+Promise地獄
易于編寫維護(hù),及時(shí)響應(yīng)
響應(yīng)式編程可以加深你代碼抽象的程度属划,讓你可以更專注于定義與事件相互依賴的業(yè)務(wù)邏輯恬叹,而不是把大量精力放在實(shí)現(xiàn)細(xì)節(jié)上,同時(shí)同眯,使用響應(yīng)式編程還能讓你的代碼變得更加簡潔绽昼。
(2)缺點(diǎn)
- 學(xué)習(xí)成本高,需要轉(zhuǎn)換思維须蜗,用流來思考
最后的最后借用尤大大的一句話
我個(gè)人傾向于在適合 Rx 的地方用 Rx硅确,但是不強(qiáng)求 Rx for everything目溉。比較合適的例子就是比如多個(gè)服務(wù)端實(shí)時(shí)消息流,通過 Rx 進(jìn)行高階處理菱农,最后到 view 層就是很清晰的一個(gè) Observable缭付,但是 view 層本身處理用戶事件依然可以沿用現(xiàn)有的范式。
FRP的思想和對(duì)事件操作的能力很不錯(cuò)循未,在需要使用的地方使用上會(huì)是錦上添花
6.參考文章
1.Rxjs思想入門
3.Rxjs光速入門