不論是用主流的前端框架業務開發仍是在寫一些 sdk,一般的編程範式都是面向對象的,尤爲 es6 新增 Class 語法糖後,功能模塊的劃分都基於類的力度。在寫過和維護過很多代碼後,漸漸覺的在狀態複雜的應用中,按局部狀態、行爲來劃分並不能讓總體代碼結構很清晰,且 js 天生的函數靈活性在類的場景下也很受約束, 因此嘗試從函數式的角度來尋找一些突破口。
用了小半年的時間,在本身相對熟悉的音視頻領域,採用函數式編程+狀態管理
的編程思路,寫了一個簡單但功能完備的 hls 播放器,算是對函數式編程有了一些理解和實踐。javascript
不論是面向對象仍是函數式,十分重要的一點是關注點分離。對於一個關注點、功能點,面向對象主要是實現細節的封裝,只對外提供簡單的 api 暴露。而對於一個關注點內部,又可分爲輕薄的控制層、對狀態抽象管理的模型層、具體業務邏輯實現,IO 操做等的服務層。前端
控制層:是各功能模塊之間交互的銜接點,串聯在一塊兒實現總體的功能,而功能模塊的劃分是否合理直接影響這一層的設計。是在 AController 中實例化一個 BController 仍是在 CController 中實例化 AController、BController?java
模型層: 對狀態的管理,對於局部狀態,遇到的最多的問題就是a.b.c,b爲undefined的運行時報錯
,代碼中充斥着大量的防護性檢測。更嚴重的是全局狀態,隨着應用的複雜,全局狀態愈來愈多,模塊依賴其餘模塊的狀態致使須要大量的 getter、setter,a.bInstance.cProp 怎麼看都不爽。react
服務層: 代碼量隨着迭代愈來愈多,代碼不容易複用,橫跨整個文件的經過 this 對屬性的獲取和修改git
面向對象的層級結構設計並不簡單、以類爲力度劃分功能帶來了各個模塊之間狀態,方法的冗餘調用、而經過 this 對狀態的處理路徑也難以跟蹤、限制了函數的靈活性
很好的講函數式的書程序員
函數式以函數爲主,講究把一個大的功能模塊拆分紅一個個小的函數,再由這些小函數組合成完整的功能。使用函數來抽象操做和控制流程。es6
操做: 函數在數學層面表明值的映射y=f(x)
,在函數式層面重在引用透明
,即函數內的操做只依賴輸入參數,不受其餘外部狀態影響,保證函數的純粹性,我認爲這是不現實的....,不可能把全部的依賴都以參數的形式傳入函數,函數的結果也不僅是產生一個新的值。參見下面對狀態的管理。github
控制流:函數式的強大在我看來在於對控制流的抽象,使得在對狀態的處理過程當中(同步的計算邏輯、異步的操做等),能以統一的口徑在各個函數中流轉,最終產生結果
編程
函數式範式重在思惟的轉換,由命令式轉向聲明式,命令式給人的感受是從一個方法進入另外一個方法,層層遞進,愈來愈深,是一種縱向的概念,而
函數式是把全部操做都放在一個水平面上,在同一水平面,數據從一個流程進入下一個流程,是一種橫向的概念,包括對同步的處理,異步的處理,產生反作用的IO操做,都抽象在一個維度
***仍是從最基本的看起。。。。*****redux
小函數組合成大功能,面臨的第一個問題是參數的數量
, y=f(x) z=f1(t,y) n=f2(z),把 f,f1,f2 組合在一塊兒由參數 x 獲得結果 n,中間過程是匹配不上的,那就規定組合的函數都只接受一個參數吧!(對於接受多個參數的函數,經過 curry,暫存前面的參數,轉換成只接受最後一個參數的部分函數)
const curry = fn => { let len = fn.length; return function _curry(...args) { if (args.length < len) { return _curry.bind(null, ...args); } return fn.apply(null, args); }; }; const compose = (...fns) => { const fnReversed = fns.reverse(); return args => { return fnReversed.reduce((ret, fn) => fn(ret), args); }; }; y=f(x) z=f1(t,y) n=f2(z) ---> let f1_1 = curry(f1)(t); let getN = compose( f2, f1_1, f ) getN(x) = n
對控制流的處理纔是函數式的優雅所在,單純的函數組合並不能處理複雜的流程,能將控制流與操做抽象在同一水平面,須要藉助容器
的概念,容器做爲輸入值的載體,容器上定義一些統一的接口,對輸入值應用某些操做,而且數據能夠從一種容器進入另外一種容器進行進一步操做
針對不一樣的場景,容器又可細分爲不一樣的子類,子類提供統一的接口不一樣的實現,根據存儲值的不一樣狀態,調用相同的API卻執行不一樣操做
class Container { constructor(v){ this._value = v; } static of(v){ return new Container(v) } map(f){ return new Container(f(this._value)) } } Continer 定義map方法,對存儲的值應用一個fn **對於帶有map方法的這一類數據結構叫作 functor,Array 有 map方法,Array就是一個functor** Container.of(1).map(x=>x+1) --> Container(2)
Container 的衍生 Maybe、Either、Task、IO 等
Maybe: 專一處理空值監測,能夠很好的處理 a.b.c 的問題
Either: 專一處理異常
Task: 異步處理,相似 Promise, 參見實現,單元測試
IO: 專一對反作用的處理
class Maybe { static of(value) { if (value === undefined || value === null) { return Empty.of(); } return Just.of(value); } } class Empty extends Maybe { static of(value) { return new Empty(value); } map() { return this; } join() { return this; } chain() { return this; } ap() { return this; } value() { return this._value; } getOrElse(f) { if (typeof f === 'function') { return f(); } return f; } toString() { return 'Empty'; } } class Just extends Maybe { static of(value) { return new Just(value); } map(fn) { const v = fn(this._value); return Maybe.of(v); } join() { return this.value(); } chain(f) { return this.map(f).join(); } ap(f) { return f.map(this.value()); } getOrElse(f) { let v = this.value(); if (typeof f === 'function' && v && v.constructor === Empty) { return f(v.value()); } return this.value(); } } eg: Maybe.of(null).map(() => {}); // do nothing Maybe.of(1).map(x => x + 1); // Maybe(2) // Just Empty 提供相同的API,對於不一樣的輸入值,空值檢測發生在內部,自動選擇使用不一樣的容器,針對對相同的操做,爲空時自動略過 eg: // 處理if邏輯判斷 maybe( ()=>{ //levels not exist,do some things,eg: load master m3u8 }, levels=>{ // levels exist,do some things with levels }, Maybe.of(store).map(prop('levels)) )
在實際使用中,咱們能夠把全部狀態數據存儲在中心 store 中,而從 store 中 getState()獲取到的數據都是 Maybe 化的
,對數據的操做和子屬性的訪問經過 map(f),這樣能夠很好的避免a.b.c
類的運行時異常
對 Either、Task 等介紹可參見 上文提到的 很好的講函數式的書,另 本身對函數式基本組件的封裝
curry,在這裏主要用於簡化函數組合的複雜性,還有延遲執行,部分暫存等用處compose,相似於傳送帶,將數據抽象在同一水平面流轉
容器,相似於傳送帶上一個個小盒子,提供統一的接口標準,使數據從一個盒子無縫進入另外一個盒子,完成操做和流程控制
上面將函數式的處理流程比喻成狀態(數據)在傳送帶上流轉,但前端應用是複雜的,咱們會有不少條傳送帶,各傳送帶之間會有狀態的交互,如何能很好的將全局狀態分發到各傳送帶?
在實踐中,借鑑了 react-redux 的思想,提供一箇中心 Store 的功能,各模塊從 store 中 getState,發送命令對 store 中數據進行更新,store 和各函數式模塊經過 connect 鏈接.
import { combineActions, combineStates, createStore } from 'vod-fp-utility';
let store = createStore(initState,actions) let {id,connect,dispatch,getState,getConfig,subscribe.subOnce} = store; connect:// `將store實例注入科裏化後的功能模塊函數,始終做爲科裏化的函數第一個參數` dispatch:// 執行命令操做,能夠是修改store的某個狀態,能夠是分發某個事件 getState: //從store中獲取狀態 subscribe:// 訂閱某個事件,響應dispatch getConfig:// 相似getState。只用來獲取config配置信息 subOnce://相似subscribe,只監聽執行一次 **connect是做爲狀態管理和函數式結合重要的中間橋樑!!!** 使用: import {initState,ACTIONS} from "./store.js" const store = createStore(initState,ACTIONS) const manageHls = curry(({ dispatch, connect }, media, url)=>{ // 這裏,manageHls中能夠輕鬆的從 store中獲取state,dispatch動做 // 經過`connect` loadPlaylist,createMediaSource等,在loadPlaylist和createMediaSource中 // 能夠一樣的和中心store進行交互 Task.resolve(connect(bootstrap)) .ap(connect(loadPlaylist)(url)) .ap(connect(createMediaSource)(media)) .error(e => { dispatch(ACTION.ERROR, e); }); }) store.connect(manageHls)(videNode,m3u8Url)
看一個例子
簡化的需求背景:
hls 點播播放有標清、高清等檔位,切換檔位時,1. 先檢查檔位信息是否存在,2. 不存在要請求檔位 m3u8 文件,解析 m3u8 3. 存在的話直接切換
可能存在異常的場景: 1. http 請求失敗 2. m3u8 解析失敗
it('# test transform Task -> Either -> Task', done => { let store = {}; let loadSuccessSpy = chai.spy(); let changeSuccessSpy = chai.spy(); let loadErrorFlag = 'loadSourceError'; let parseM3u8ErrorFlag = 'parseM3u8Error'; let parsedM3u8Data = 'parsedM3u8Data'; let getState = key => Maybe.of(store).map(prop(key)); let setState = (key, v) => (store[key] = v); let _doStoreLevels = text => { store['levels'] = text; return text; }; let _loader = flag => { return Task.of((resolve, reject) => { setTimeout( () => (flag === loadErrorFlag ? reject(flag) : resolve(flag)), 200 ); }); }; let parseM3u8 = flag => { if (flag === parseM3u8ErrorFlag) { return Fail.of(flag); } return Success.of(flag); }; // loadSource :: boolean -> (Task(error) | Either(success|error)) let loadSource = flag => { return _loader(flag) .chain(parseM3u8) .map(_doStoreLevels) .map(x => { loadSuccessSpy(); return x; }); }; // changePlaylist :: boolean -> (Either(success) | loadSource) let changePlaylist = flag => { return maybe( () => loadSource(flag), levels => { changeSuccessSpy(); return Success.of(levels); }, getState('levels') ); }; changePlaylist(loadErrorFlag).error(e => { e.should.be.equal(loadErrorFlag); loadSuccessSpy.should.not.be.called(); changeSuccessSpy.should.not.be.called(); }); setTimeout(() => { changePlaylist(parseM3u8ErrorFlag).error(e => { e.should.be.equal(parseM3u8ErrorFlag); changeSuccessSpy.should.not.be.called(); loadSuccessSpy.should.not.be.called(); }); }, 350); setTimeout(() => { changePlaylist(parsedM3u8Data).map(x => { x.should.be.equal(parsedM3u8Data); loadSuccessSpy.should.be.called.once; changeSuccessSpy.should.not.be.called(); }); }, 700); setTimeout(() => { changePlaylist(parsedM3u8Data).map(x => { x.should.be.equal(parsedM3u8Data); loadSuccessSpy.should.be.called.once; changeSuccessSpy.should.be.called(); done(); }); }, 1000); });
本文並不能讓你對函數式有多少了解,至少我本身目前也只有一些基本的認識(雖然這本書看了兩三遍)),但函數式的思想仍是值得在項目中不斷實踐的。