摘要:本文是響應(yīng)式編程第二章序列的深入研究這篇文章的學(xué)習(xí)筆記。函數(shù)科里化的基本應(yīng)用,也是函數(shù)式編程中運(yùn)算管道構(gòu)建的基本方法。四資料參考函數(shù)式編程指南
本文是Rxjs 響應(yīng)式編程-第二章:序列的深入研究這篇文章的學(xué)習(xí)筆記。一. 劃重點(diǎn)示例代碼托管在:http://www.github.com/dashnowords/blogs
更多博文:《大史住在大前端》目錄
文中使用到的一些基本運(yùn)算符:
map-映射
filter-過(guò)濾
reduce-有限列聚合
scan-無(wú)限列聚合
flatMap-拉平操作(重點(diǎn))
catch-捕獲錯(cuò)誤
retry-序列重試
from-生成可觀測(cè)序列
range-生成有限的可觀測(cè)序列
interval-每隔指定時(shí)間發(fā)出一次順序整數(shù)
distinct-去除出現(xiàn)過(guò)的重復(fù)值
建議自己動(dòng)手嘗試一下,記住就可以了,有過(guò)lodash使用經(jīng)驗(yàn)的開(kāi)發(fā)者來(lái)說(shuō)并不難。原文中使用flatMap轉(zhuǎn)換序列時(shí)有一處應(yīng)該是手誤:
二. flatMap功能解析原文中在http請(qǐng)求拿到獲取到數(shù)據(jù)后,最初使用了forEach實(shí)現(xiàn)了手動(dòng)流程管理,于是原文提出了優(yōu)化設(shè)想,試圖探究如何依賴(lài)響應(yīng)式編程的特性將手動(dòng)的數(shù)據(jù)加工轉(zhuǎn)換改造為對(duì)流的轉(zhuǎn)換,好讓最終的消費(fèi)者能夠拿到直接可用的數(shù)據(jù),而不是得到一個(gè)響應(yīng)后手動(dòng)進(jìn)行很多后處理。在代碼層面需要解決的問(wèn)題就是,如何在不使用手動(dòng)遍歷的前提下將一個(gè)有限序列中的數(shù)據(jù)逐個(gè)發(fā)給訂閱者,而不是一次性將整個(gè)數(shù)據(jù)集發(fā)過(guò)去。
假設(shè)我們現(xiàn)在并不知道有flatMap這樣一個(gè)可以使用的方法,那么先來(lái)做一些嘗試:
var quakes = Rx.Observable.create(function(observer) { //模擬得到的響應(yīng)流 var response = { features:[{ earth:1 },{ earth:2 }], test:1 } /* 最初的手動(dòng)遍歷代碼 var quakes = response.features; quakes.forEach(function(quake) { observer.onNext(quake); });*/ observer.onNext(response); }) //為了能將features數(shù)組中的元素逐個(gè)發(fā)送給訂閱者,需要構(gòu)建新的流 .map(dataset){ return Rx.Observable.from(dataset.features) }
當(dāng)我們訂閱quakes這個(gè)事件流的時(shí)候,每次都會(huì)得到另一個(gè)Observable,它是因?yàn)閿?shù)據(jù)源經(jīng)過(guò)了映射變換,從數(shù)據(jù)變成了可觀測(cè)對(duì)象。那么為了得到最終的序列值,就需要再次訂閱這個(gè)Observable,這里需要注意的是可觀測(cè)對(duì)象被訂閱前是不啟動(dòng)的,所以不用擔(dān)心它的時(shí)序問(wèn)題。
quakes.subscribe(function(data){ data.subscribe(function(quake){ console.log(quake); }) });
如果將Observable看成一個(gè)盒子,那么每一層盒子只是實(shí)現(xiàn)了流程控制功能性的封裝,為了取得真正需要使用的數(shù)據(jù),最終的訂閱者不得不像剝洋蔥似的通過(guò)subscribe一層層打開(kāi)盒子拿到最里面的數(shù)據(jù),這樣的封裝性對(duì)于數(shù)據(jù)在流中的傳遞具有很好的隔離性,但是對(duì)最終的數(shù)據(jù)消費(fèi)者而言,卻是一件很麻煩的事情。
這時(shí)flatMap運(yùn)算符就派上用場(chǎng)了,它可以將冗余的包裹除掉,從而在主流被訂閱時(shí)直接拿到要使用的數(shù)據(jù),從大理石圖來(lái)直觀感受一下flatMap:
乍看之下會(huì)覺(jué)得它和merge好像是一樣的,其實(shí)還是有一些區(qū)別的。merge的作用是將多個(gè)不同的流合并成為一個(gè)流,而上圖中A1,A2,A3這三個(gè)流都是當(dāng)主流A返回?cái)?shù)據(jù)時(shí)新生成的,可以將他們想象為A的支流,如果你想在支流里撈魚(yú),就需要在每個(gè)支流里布網(wǎng),而flatMap相當(dāng)于提供了一張大網(wǎng),將所有A的支流里的魚(yú)都給撈上來(lái)。
所以在使用了flatMap后,就可以直接在一級(jí)訂閱中拿到需要的數(shù)據(jù)了:
var quakes = Rx.Observable.create(function(observer) { var response = { features:[{ earth:1 },{ earth:2 }], test:1 } observer.onNext(response); }).flatMap((data)=>{ return Rx.Observable.from(data.features); }); quakes.subscribe(function(quake) { console.log(quake) });三. flatMap的推演 3.1 函數(shù)式編程基礎(chǔ)知識(shí)回顧
如果本節(jié)的基本知識(shí)你尚不熟悉,可以通過(guò)javascript基礎(chǔ)修煉(8)——指向FP世界的箭頭函數(shù)這篇文章來(lái)簡(jiǎn)單回顧一下函數(shù)式編程的基本知識(shí),然后再繼續(xù)后續(xù)的部分。
/*map運(yùn)算符的作用 *對(duì)所有容器類(lèi)而言,它相當(dāng)于打開(kāi)容器,進(jìn)行操作,然后把容器再蓋上。 *Container在這里只是一個(gè)抽象定義,為了看清楚它對(duì)于容器中包含的值意味著什么。 *你會(huì)發(fā)現(xiàn)它其實(shí)就是Observable的抽象原型。 */ Container.prototype.map = function(f){ return Container.of(f(this.__value)) } //基本的科里化函數(shù) var curry = function(fn){ args = [].slice.call(arguments, 1); return function(){ [].push.apply(args, arguments); return fn.apply(this, args); } } //map pointfree風(fēng)格的map運(yùn)算符 var map = curry(function(f, any_functor_at_all) { return any_functor_at_all.map(f); }); /*compose函數(shù)組合方法 *運(yùn)行后返回一個(gè)新函數(shù),這個(gè)函數(shù)接受一個(gè)參數(shù)。 *函數(shù)科里化的基本應(yīng)用,也是函數(shù)式編程中運(yùn)算管道構(gòu)建的基本方法。 */ var compose = function (f, g) { return function (x) { return f(g(x)); } }; /*IO容器 *一個(gè)簡(jiǎn)單的Container實(shí)現(xiàn),用來(lái)做流程管理 *這里需要注意,IO實(shí)現(xiàn)的作用是函數(shù)的緩存,且總是返回新的IO實(shí)例 *可以看做一個(gè)簡(jiǎn)化的Promise,重點(diǎn)是直觀感受一下它作為函數(shù)的 *容器是如何被使用的,對(duì)于理解Observable有很大幫助 */ var IO = function(f) { this.__value = f; } IO.of = function(x) { return new IO(function() { return x; }); } IO.prototype.map = function(f) { return new IO(compose(f, this.__value)); }
如果上面的基本知識(shí)沒(méi)有問(wèn)題,那么就繼續(xù)。
3.2 從一個(gè)容器的例子開(kāi)始現(xiàn)在來(lái)實(shí)現(xiàn)這樣一個(gè)功能,讀入一個(gè)文件的內(nèi)容,將其中的a字符全部換成b字符,接著存入另一個(gè)文件,完成后在控制臺(tái)輸出一個(gè)消息,為了更明顯地看到數(shù)據(jù)容器的作用,我們使用同步方法并將其包裹在IO容器中,然后利用函數(shù)式編程:
var fs = require("fs"); //讀取文件 var readFile = (filename)=>IO.of(fs.readFileSync(filename,"utf-8")); //轉(zhuǎn)換字符 var transContent = (content)=>IO.of((content)=>content.replace("a","b")); //寫(xiě)入字符串 var writeFile = (content)=>IO.of(fs.writeFileSync("dest.txt",content));
當(dāng)具體的函數(shù)被IO容器包裹起來(lái)而實(shí)現(xiàn)延遲執(zhí)行的效果時(shí),就無(wú)法按原來(lái)的方式使用compose( )運(yùn)算符直接對(duì)功能進(jìn)行組合,因?yàn)?b>readFile函數(shù)運(yùn)行時(shí)的輸出結(jié)果(一個(gè)io容器實(shí)例)和transContent函數(shù)需要的參數(shù)類(lèi)型(字符串)不再匹配,在不修改原有函數(shù)定義的前提下,函數(shù)式編程中采用的做法是使用map操作符來(lái)預(yù)置一個(gè)參數(shù):
/* *map(transContent)是一個(gè)高階函數(shù),它的返回函數(shù)就可以接收一個(gè)容器實(shí)例, *并對(duì)容器中的內(nèi)容執(zhí)行map操作。 */ var taskStep12 = compose(map(transContent), readFile);
這里比較晦澀,涉及到很多功能性函數(shù)的嵌套,建議手動(dòng)推導(dǎo)一下taskStep12這個(gè)變量的值,它的結(jié)構(gòu)是這樣一種形式:
io{ __value:io{ __value:someComposedFnExpression } }
如果試圖一次性將所有的步驟組合在一起,就需要采用下面的形式:
var task = compose(map(map(writeFile)),map(transContent),readFile); //組合后的task形式就是 //io{io{io{__value:someComposedFnExpression}}}
問(wèn)題已經(jīng)浮出水面了,每多加一個(gè)針對(duì)容器操作的步驟,書(shū)寫(xiě)時(shí)就需要多包裹一層map,而運(yùn)行時(shí)就需要多進(jìn)入一層才能觸及組合好的可以實(shí)現(xiàn)真正功能的函數(shù)表達(dá)式,真的是很麻煩。
提示一:3.3 Monad登場(chǎng)現(xiàn)在來(lái)回想一下原示例中的Observable對(duì)象,將其看做是一個(gè)容器(含有map類(lèi)方法),那么如果map方法調(diào)用時(shí)傳入的參數(shù)是一個(gè)運(yùn)行時(shí)會(huì)生成新的Observable對(duì)象的方法時(shí),就會(huì)產(chǎn)生Observable嵌套,得到observable{observable{.....}}這樣的結(jié)構(gòu),那么在最終的數(shù)據(jù)消費(fèi)者通過(guò)subscribe方法訂閱數(shù)據(jù)時(shí),就不得不用很多個(gè)subscribe才能拿到實(shí)際需要的數(shù)據(jù)。
提示二:
沒(méi)有相關(guān)經(jīng)驗(yàn)的讀者在使用pointfree風(fēng)格的map操作符時(shí)可能會(huì)感到非常不適應(yīng),如果你覺(jué)得它很難理解,也可以嘗試直接使用IO.prototype.map這種鏈?zhǔn)秸{(diào)用風(fēng)格的寫(xiě)法將上例中的三個(gè)步驟組合在一起來(lái)查看最后的結(jié)果,畢竟在Rxjs中常使用的也就是Observable這一個(gè)容器類(lèi)。
當(dāng)我們看到問(wèn)題所在后就不難發(fā)現(xiàn),其實(shí)這個(gè)問(wèn)題的解決方法并不復(fù)雜,我們要做的不過(guò)就是在必要的時(shí)候合并內(nèi)容的容器,為此來(lái)定義兩個(gè)合并運(yùn)算的方法:
//鏈?zhǔn)秸{(diào)用風(fēng)格 IO.prototype.join = function(){ return this.isNothing() ? IO.of(null):this.__value; } //pointfree風(fēng)格運(yùn)算符 var join = (m)=>m.join();
這里引入一個(gè)新的概念Monad,它的定義是可以被展平的容器,也就是說(shuō)擁有join和of方法并遵循一定規(guī)則的容器,都是Monad,在這種設(shè)定下,3.1中的示例就可以被改寫(xiě)為下面的形式:
var task = compose(join,map(writeFile),join,map(transContent),readFile);
不難發(fā)現(xiàn)map和join總是需要成對(duì)出現(xiàn)的,那么再利用函數(shù)科里化的技巧將map和join連起來(lái):
var chain = curry(function(f,m){ return m.map(f).join(); })
那么組合后的函數(shù)就變成了下面的形式:
var task = compose(chain(writeFile),chain(transContent),readFile);
這里的chain,就是FlatMap。
3.4 對(duì)比總結(jié)最后將上面幾種形式放在一起再來(lái)回顧一下:
//原有形式 var task = compose(map(map(writeFile)),map(transContent),readFile); //map-join形式 var task = compose(join,map(writeFile),join,map(transContent),readFile); //chain形式(flatMap) var task = compose(chain(writeFile),chain(transContent),readFile);
如果理解了這幾種形式,就不難理解flatMap的拉平效應(yīng)了,所謂flatMap,說(shuō)白了其實(shí)就是將容器展開(kāi)的一種操作。
3.5 一點(diǎn)疑問(wèn)flatMap所解決問(wèn)題,是在函數(shù)式編程引入了Functor的概念將邏輯函數(shù)包裹在容器中后才產(chǎn)生的,那么這種容器概念的引入對(duì)函數(shù)式編程到底有什么意義,筆者尚未搞清楚,相關(guān)內(nèi)容留作以后補(bǔ)充。
四. 資料參考《javascript函數(shù)式編程指南》https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/105390.html
摘要:本文是響應(yīng)式編程第三章構(gòu)建并發(fā)程序這篇文章的學(xué)習(xí)筆記。筆者在自己的實(shí)現(xiàn)中又加入了右鍵切換飛船類(lèi)型的功能,必須得說(shuō)開(kāi)發(fā)游戲的確比寫(xiě)業(yè)務(wù)邏輯要有意思。由于沒(méi)有精確計(jì)算雪碧圖的坐標(biāo),所以在碰撞檢測(cè)時(shí)會(huì)有一些偏差。 本文是Rxjs 響應(yīng)式編程-第三章: 構(gòu)建并發(fā)程序這篇文章的學(xué)習(xí)筆記。示例代碼托管在:http://www.github.com/dashnowords/blogs 更多博文:《大...
摘要:本文是響應(yīng)式編程第四章構(gòu)建完整的應(yīng)用程序這篇文章的學(xué)習(xí)筆記。涉及的運(yùn)算符每隔指定時(shí)間將流中的數(shù)據(jù)以數(shù)組形式推送出去。中提供了一種叫做異步管道的模板語(yǔ)法,可以直接在的微語(yǔ)法中使用可觀測(cè)對(duì)象示例五一點(diǎn)建議一定要好好讀官方文檔。 本文是【Rxjs 響應(yīng)式編程-第四章 構(gòu)建完整的Web應(yīng)用程序】這篇文章的學(xué)習(xí)筆記。示例代碼托管在:http://www.github.com/dashnoword...
摘要:就像我寫(xiě)書(shū)的過(guò)程一樣,每個(gè)開(kāi)發(fā)者在學(xué)習(xí)函數(shù)式編程的旅程中都會(huì)經(jīng)歷這個(gè)部分。類(lèi)型在函數(shù)式編程中有一個(gè)巨大的興趣領(lǐng)域類(lèi)型論,本書(shū)基本上完全遠(yuǎn)離了該領(lǐng)域。在函數(shù)式編程中,像這樣涵蓋是很普遍的。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關(guān)于譯者:這是一個(gè)流淌著滬江血液的純粹工程:認(rèn)真,是 HTML...
摘要:在函數(shù)式編程中數(shù)據(jù)在由純函數(shù)組成的管道中傳遞。函數(shù)式編程中函子是實(shí)現(xiàn)了函數(shù)的容器下文中將函子視為范疇,模型可表示如下但是在函數(shù)式編程中要避免使用這種面向?qū)ο蟮木幊谭绞饺《畬?duì)外暴露了一個(gè)的接口也稱(chēng)為。 showImg(https://segmentfault.com/img/remote/1460000018101204); 該系列會(huì)有 3 篇文章,分別介紹什么是函數(shù)式編程、剖析函數(shù)...
閱讀 1485·2021-10-13 09:39
閱讀 1408·2021-09-23 11:22
閱讀 2311·2019-08-30 14:05
閱讀 1130·2019-08-29 17:03
閱讀 869·2019-08-29 16:24
閱讀 2293·2019-08-29 13:51
閱讀 720·2019-08-29 13:00
閱讀 1431·2019-08-29 11:24