不论是用主流的前端框架业务开发仍是在写一些 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); });
本文并不能让你对函数式有多少了解,至少我本身目前也只有一些基本的认识(虽然这本书看了两三遍)),但函数式的思想仍是值得在项目中不断实践的。