函数式反应型编程(FRP)
函數式反應型編程(FRP)
前言
當我們開始寫項目的時候,總會遇到一些情景,比如定時任務,ajax請求,要求我們去請求大的文件或者圖片這些,然后我們就發現,直接寫代碼,會出現加載緩慢,白屏這樣的問題,眾所周知,js是單線程的,所以js任務也是一個一個順序執行的,就是同步執行,后一個任務必須等待前一個任務完成之后才能執行,就如前面所說的,如果前一個任務花費的時間很長,就會造成阻塞,給用戶帶來不好的體驗,我們要解決這些問題,當我們等待但是又不要阻塞程序,所以就需要異步處理這些耗時間的任務。
1.處理異步的方法
假設有多個異步任務,且任務有依賴關系,后一個任務必須拿到前一個任務的執行結果才可以執行
作為剛剛入門學習的小白,比如我,最先能想到的就是函數調用了
(1)回調:容易造成回調地獄
回調是實現異步編程最簡單的方法,我們可以在一個函數中調用其他函數,實現我們想要的邏輯
getData1(data1 => {getData2(data1, data2 => {getData3(data2, data3 => {getData4(data3, data4 => {getData5(data4, data5 => {// 終于取到data5了})})})}) })乍一看,寫的還可以,也達到了預期的效果,就是看起來不美觀,而且還形成了回調地獄,但是如果里面出現很多問題,需要進行異常處理及各種邏輯檢查呢,那這一塊代碼就會變得非常長,非常復雜,可能下次同事看見了你的代碼,直接暴走。
為了讓代碼更具備可讀性,Promise隆重登場。
(2)Promise:解決回調地獄
依舊來解決上面的問題
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); })我們直接一路鏈式調用,看起來更清楚明了,但是感覺這樣調用也不是個辦法。
你的上級詢問,那還能做的更優雅嗎?這可難住了我,喔喔喔,但是不要怕,es5的回調讓我陷入地獄,但是我爬起來了,es6的Promise讓我得到夸獎,es7這不是又出了好方法嗎?讓我們接下來看看es7的async/await吧。
(3)async/await:簡化了邏輯,但是損失了異步帶來的性能優勢(比如把并行變成串行,增加了時間開銷)
// 定義一個執行Async函數方法 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); }確實好用,也可以使用try…catch了,啊,這你想肯定能滿足上級的要求了,又簡單又優雅,小白一眼就看懂,async/await帶著我走向了人生巔峰。
(4)各個方法優缺點總結
| 回調 | 簡單邏輯處理很方便 | 邏輯復雜時容易造成回調地獄 |
| Promise | 1.狀態改變就不會再變,任何時候都能得到確切的結果 2.寫法符合思維邏輯 | 1.一旦創建,立即執行,中途無法取消 2.處于pending狀態時,無法得知狀態3.不設置回調函數,內部錯誤無法反映到外部 |
| Async/await | 1. 做到了串行的同步寫法 2.代碼清晰明了 | 1.做不到并行,除非await不在一個函數里面 2.沒有了promise的方法,比如race() 3.沒有Promise的reject方法,得寫在try…catch中 |
代碼非常簡潔易讀了,但是學海無涯,我發現現在有了一個新的技術,叫做FRP,看了一些文章,文章里面一直說有了FRP,就使用流來處理異步,把異步數據看成數據流來處理,會讓事情更簡單
那FRP到底是什么呢?
2.FRP是什么
首先讓我們先使用FRP直接實現上述的需求
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'); }})看不懂吧,看不懂沒關系,只需要知道from是建立流的,mergeMap()是操作流的,subscribe是訂閱流的,最后直接輸出結果就好了,我們先來了解一下什么是FRP及實際應用,重點是學習FRP不同的思維方式
(1)概念
FRP(Functional Reactive Programming),也叫函數式響應式編程
函數反應式編程 = 函數式編程(Functional programming)+響應式編程(Reactive Programming)
如果不知道函數式編程的朋友,推薦看這個編程指南,這邊主要講解響應式編程
響應式編程使用異步數據流編程,即將各種數據【包括http請求、DOM事件等】包裝成流的形式,用操作符對流進行操作,能用同步方式處理異步數據光看這個概念,我是完全沒法看明白的,所以需要拆開來看
(2)數據流是什么?
數據流是按時間排序的即將發生的事情的序列
舉個例子,我們寫代碼時,會對數據進行轉換運算,比如先轉成什么再轉成什么再轉成什么,轉換這整個過程相當于一個流著數據的管道,數據以流的方式在這個管道中流通,這些數據轉換我們會使用各種方法,相當于傳入數據作為函數參數轉換后得到新數據結果,最后的結果從管道中流出。
流轉換的思想為將數據事件抽象成管道中流通的流體,轉換成新的數據事件,這些事件還包含了基本的數據值,還可以進行相應的運算,這種運算讓我們不需要花時間去進行事件監聽什么的,我們只需要專注于數據的轉換,也就是事件的使用,而不是直接操作數據。
所以我們在學習這章內容的時候,還應該學會轉換思維。
總體思想:什么都可以是流,變量,用戶輸入,屬性,高速緩存,數據結構等,將時間線上的數據建模成流
(2)響應式是什么?變化傳遞(跟著變化)
vue就是響應式編程,我們只需要關注數據變化,不需要操作視圖改變,因為視圖會跟著改變
(3)觀察者模式
是一種設計模式,允許定義一種訂閱機制,可以在對象事件發生時通知多個觀察的該對象的其他對象
比如你花錢了之后銀行會給你發消息,就是觀察者模式,余額是被觀察的對象,用戶是觀察者
(4)迭代器模式
游標模式,挨著挨著一步一步運行
比如map,set,array都使用了迭代器模式
3.使用案例
前端的FRP的庫:Rxjs【比較多人使用】、Most,后續內容使用Rxjs
Rxjs中文文檔:https://cn.rx.js.org/
Rxjs英文文檔:https://rxjs.dev/
讓我們學習幾個小案例,來體會FRP的魅力吧~~
可引入也可以使用npm安裝:
RxJS 是一個庫,它通過使用 observable 序列來編寫異步和基于事件的程序。它提供了一個核心類型 Observable,附屬類型 (Observer、 Schedulers、 Subjects) 和操作符 (map、filter、reduce、every, 等等),這些數組操作符可以把異步事件作為集合來處理。
基本概念:
- Observable (可觀察對象): 表示一個概念,這個概念是一個可調用的未來值或事件的集合。
- Observer (觀察者): 一個回調函數的集合,它知道如何去監聽由 Observable 提供的值。
- Subscription (訂閱): 表示 Observable 的執行,主要用于取消 Observable 的執行。
- Operators (操作符): 采用函數式編程風格的純函數 (pure function),使用像 map、filter、concat、flatMap 等這樣的操作符來處理集合。
- Subject (主體): 相當于 EventEmitter,并且是將值或事件多路推送給多個 Observer 的唯一方式。
- Schedulers (調度器): 用來控制并發并且是中央集權的調度員,允許我們在發生計算時進行協調,例如 setTimeout 或 requestAnimationFrame 或其他。
借用官網第一個例子入門
注冊事件監聽器的常規寫法如下
var button = document.querySelector('button'); button.addEventListener('click', () => console.log('Clicked!'));使用 RxJS 的話,創建一個 observable 來代替。
var button = document.querySelector('button'); Rx.Observable.fromEvent(button, 'click').subscribe(() => console.log('Clicked!'));(1)實現計數器
? a.以前實現計數器:
? 直接想實現方法,直接定義全局變量開始寫實現細節,點擊則全局變量+1然后打印,這是我們平時思考的正常思維
let counter = 0; buttton.on("click", ()=>{counter+=1;console.log(counter); })? 缺點:
? 使用了全局變量:容易被改變值,輸入輸出不確定性,后期維護困難等
? b.有了FRP之后實現計數器:
-
已知創建流的函數formEvent
-
已知操作流的函數:pipe、map、scan、subscribe
pipe:用于鏈接可觀察的運算符
? map:類似于Array.prototypr.map(),它把每個源值傳遞給轉化函數以獲得相應的輸出值。
? scan:數組的 reduce 類似。它需要一個暴露給回調函數當參數的初始值。每次回調函數運行后的返回值會作為下次回調函數運行時的參數
? subscribe:監聽流,訂閱流
我們先在api中找到了對應的方法,formEvent直接創建一個流->用pipe進行數據流的連接(在里面可以寫事件的實現方法,我們只需要考慮怎么運用事件處理,而不需要直接去操作數據)->利用map進行初始化操作->scan進行數據相加操作->subscribe()方法訂閱整個流,最后輸出
rxjs.fromEvent(document.querySelector('.this'), 'click').pipe( // 連接運算符rxjs.operators.map((_) => 1), // 將原值全變為1,不用定義全局變量rxjs.operators.scan((sum, val) => { // 相加return sum + val;}, 0)).subscribe((x) => { console.log('got value ' + x); }); //打印? 優點:
? 和直接操作數據相比,除了創建流之外,FRP不需要有全局變量,直接可操作
(2)實現雙擊
? 例子:如果兩次click之間的間隔時間小于等于250ms,為一次雙擊,否則為兩次單擊,請在單擊、雙擊時分別log
? 以前實現雙擊:我們會考慮時間戳,判斷點擊事件的間隔時間是否小于等于250ms,然后進行判斷,但是會出現問題,如果連點三次,判斷上就會出現問題,或者設置標志位,但是不管哪種方式實現,都會有些困難
<input type="button" onclick="aa()" ondblclick="bb()" value="點我"> <script language="javascript"> var isdb; //設置變量 function aa(){ isdb=false; //標志位window.setTimeout(cc, 250) //這里調用windowfunction cc(){ if(isdb!=false)return; console.log("單擊") } } function bb(){ isdb=true; console.log("雙擊") } </script>? FRP實現雙擊:
- 已知操作流的函數:debounceTime、buffer、filter
? debounceTime:去抖動的作用,控制發送頻率操作
? buffer:將過往的值收集到一個數組中
? filter:類似于 Array.prototype.filter(), 它只會發出符合標準函數的值。
雙擊事件也是操作api,有直接可以使用的去抖動方法debounceTime(),思考流程應該是點擊事件創建流->然后這個流去抖動->收集去抖動的值->判斷產生的每個數組長度,等于2就是雙擊,同理可得等于1就是單擊
var button = document.querySelector('.this');var clickStream = rxjs.fromEvent(button, 'click'); //創建流var doubleClickStream = 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 === 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)實現拖動
? 請使用mousedown、mousemove、mouseup事件來實現“鼠標拖動時,log:draging”
? a.以前實現:js實現拖拽,以前實現拖拽,起碼是一兩百行代碼起步,而且邏輯判斷上可能還會出現問題
? b.現在FRP實現:
-
已知操作流的函數:flatMap【現在已變成mergeMap】、takeUntil
flatmap:每個流進行運算然后合并輸出
takeUntil:先發出一個流的值,直到第二個流發出值,就完成
但是當我們使用Rxjs實現的時候,代碼實現就會變得很少,只需要幾行就可以實現需求,如下所示【具體理解上可能會有些困難,學習具體的推薦從官方中文文檔入手,比較詳細】
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
從處理異步的方法上,我們發現async/await并不擅長處理并行需求,雖然也可以處理,但是耗費時間多些,但是FRP操作符,對于并行串行都可以很適用
流處理方式的價值且遠不止于此,對于事件處理也非常適用,響應式編程的思維方式也是非常有價值的一點
FRP的特性總結如下:
-
純凈性 (Purity)
使得 RxJS 強大的正是它使用純函數來產生值的能力。這意味著你的代碼更不容易出錯。
通常你會創建一個非純函數,在這個函數之外也使用了共享變量的代碼,這將使得你的應用狀態一團糟。
-
流動性 (Flow)
RxJS 提供了一整套操作符來幫助你控制事件如何流經 observables 。
5.總結
(1)優點
-
抽象層面更高
FRP以流為單位,封裝了時間序列和具體的數據,隱藏了“狀態的同步”、“異步邏輯的具體實現”等底層細節。
-
和函數式編程配合使用
能夠使用組合,像管道處理一樣處理各種流,符合函數式編程的思維。
-
提供非阻塞、異步特性,便于處理異步情景,但是得是有非常復雜的異步情景時才適用,平時的簡單異步請求,90%都是可以被async/await還有Promise解決的
-
避免模式混用,回調和promise混用、全局變量和局部變量混用,最后可能成為無盡的callback+Promise地獄
-
易于編寫維護,及時響應
響應式編程可以加深你代碼抽象的程度,讓你可以更專注于定義與事件相互依賴的業務邏輯,而不是把大量精力放在實現細節上,同時,使用響應式編程還能讓你的代碼變得更加簡潔。
(2)缺點
- 學習成本高,需要轉換思維,用流來思考
最后的最后借用尤大大的一句話
我個人傾向于在適合 Rx 的地方用 Rx,但是不強求 Rx for everything。比較合適的例子就是比如多個服務端實時消息流,通過 Rx 進行高階處理,最后到 view 層就是很清晰的一個 Observable,但是 view 層本身處理用戶事件依然可以沿用現有的范式。FRP的思想和對事件操作的能力很不錯,在需要使用的地方使用上會是錦上添花
6.參考文章
1.Rxjs思想入門
2.你一直都錯過的反應型編程
3.Rxjs光速入門
4.響應式編程介紹
總結
以上是生活随笔為你收集整理的函数式反应型编程(FRP)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 缠论入门到精通理论到实战
- 下一篇: Linux: shell 中命令代换 $