函数式编程及其在react中的应用

栏目: 编程语言 · 发布时间: 6年前

内容简介:开头:本来想写一篇介绍redux的分享,结果阅读源码时发现看懂源码还必须先对函数式编程有一定的了解,结果写着写着就变成了一篇介绍函数式编程的文章,也罢...很多人(包括我自己)开始正面面对‘函数式’这个名词都是从使用react开始。现如今react已经成为了名副其实最受欢迎的前端框架之一,而它给我们带来的并不仅仅是虚拟dom,组件化等革命式的特性,也潜移默化地将函数式编程引入大众jser的眼中。那函数式编程到底是何方神圣?

开头:本来想写一篇介绍redux的分享,结果阅读源码时发现看懂源码还必须先对函数式编程有一定的了解,结果写着写着就变成了一篇介绍函数式编程的文章,也罢...

函数式编程在js中展露锋芒

很多人(包括我自己)开始正面面对‘函数式’这个名词都是从使用react开始。现如今react已经成为了名副其实最受欢迎的前端框架之一,而它给我们带来的并不仅仅是虚拟dom,组件化等革命式的特性,也潜移默化地将函数式编程引入大众jser的眼中。

那函数式编程到底是何方神圣?

开始

"函数式编程"是一种"编程范式",也就是如何编写程序的方法论,就像我们熟知的面向对象编程一样。

在过去很长一段时间里函数式编程是一种只存在于理论的规范, 但是近年来随着技术的发展,函数式编程已经在实际生产中发挥巨大的作用了,越来越多的语言开始加入闭包,匿名函数等非常典型的函数式编程的特性,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。

命令式or声明式

命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节,面向的是过程。 与命令式不同,声明式意味着我们要写表达式,而不是一步一步的指示

注:表达式"是一个单纯的运算过程,总是有返回值;"语句"是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

// 命令式
var makes = [];
  for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 声明式
var makes = cars.map(function(car){ return car.make; });
复制代码

声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。更加清晰和简洁之外,我们一眼就能知道它大概做了些什么,而不需要关注他怎么做

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的不函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,往往给我们带来灾难性的后果。

清楚这些之后我们再来看看在react中是如何使用声明式的:

function TodoList() {
  const todos = ['finish doc', 'submit pr', 'nag dan to review'];
  return (
    <ul>
      {todos.map((message) => <Item key={message} message={message} />)}
    </ul>
  );
}
复制代码

这是react官方文档摘录的一个例子,看吧react一开始就‘欺骗’我们使用函数式编程,实际上js在es5加入map/reduce/filter等数组操作时就已经渐渐的向函数式靠拢了

函数是‘一等公民’

当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样...所 以就是普通公民(坐经济舱的人?)。函数真没什么特殊的,你可以像对待任何其 他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量...等 等。 --摘自JS函数式指南

我们再来看看高阶函数的要求:

  • 接受一个或多个函数作为输入
  • 输出一个函数

好的,再次不谋而合。 什么?你说早已用过高阶函数?

$.ajax("http://xxx.com/getUserInfo?" + userId, function(data) {
        if (typeof callback === "function") {
            callback(data);
        }
    });
复制代码

是的这可是非常有用的经验之谈。高阶函数、高阶函数也不过是仅此而已,是的js中基于异步编程恰恰是一种函数式。

而在react中对应高阶函数就是高阶组件(hoc),顾名思义就是一个接受组件为参数并且返回一个组件的函数,是一种代替mixin来实现公用逻辑的技巧。

最最常见的例子:react-redux中的connect函数

connect(
    state => state.user,
    { action }
)(App)
复制代码

其作用就是在组件APP外层包裹了一个用以获取redux储存的state和action的容器,我们通常称他们为容器组件 当然我们也可以发挥想象,自由发挥,自定义我们的hoc,

神圣不可‘污染’的纯函数

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

再次强调“纯”

比如 slice 和 splice ,这两个函数的作用并无二致——但是注意,它们各自 的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定 义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的 那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变 了。

var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
xs.slice(0,3);
//=> [1,2,3]
// 不纯的
xs.splice(0,3);
//=> [1,2,3]
xs.splice(0,3);
//=> [4,5]
xs.splice(0,3)
//=>[]
复制代码

还不够明显?

// 不纯的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 纯的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
复制代码

在不纯的版本中, checkAge 的结果将取决于 minimum 这个可变变量的值。换 句话说,它取决于外界状态;这一点令人沮丧,因为它引入了外 部的环境,从而增加了认知负荷。

当然说了这么多,纯函数给我们到底带来了哪些好处,我们在实际中又是怎么加以利用的?

1.可缓存性

import _ from 'lodash';
var sin = _.memorize(x => Math.sin(x));

//第一次计算的时候会稍慢一点
var a = sin(1);

//第二次有了缓存,速度极快
var b = sin(1);
复制代码

利用了lodash的memorize函数,记忆住了sin(1)的值,以来加快了运行速度,提升了性能。

说好的react中应用呢?别急

reselect

我们知道,redux state的任意改变都会导致所有容器组件的mapStateToProps的重新调用,进而导致使用到selectors重新计算,但state的一次改变只会影响到部分seletor的计算值,只要这个selector使用到的state的部分未发生改变,selector的计算值就不会发生改变,理论上这部分分计算时间是可以被节省的。 reselect正是用来解决这个问题的,它可以创建一个具有记忆功能的selector,但他们的计算参数并没有发生改变时,不会再次计算,而是直接使用上次缓存的结果。从而优化了性能。

2.可测试性

没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修改什么值给其他函数继续使用(在指令式编程中可以用类成员或是全局变量做到)。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。 这简直是单元测试梦寐以求的啊

3.引用透明

纯函数是完全自给自足的,它需要的所有东西都能轻易获得。仔细思考思考这一 点...这种自给自足的好处是什么呢?首先,纯函数的依赖很明确,因此更易于观察 和理解——没有偷偷摸摸的小动作(不可预测地改变外界环境)。

我们再来介绍一个为‘纯’(不可变对象)而生的技巧:immutable.js

Immutable.js的作用在于更加高效的方式创建不可变对象,主要的有点有三个

  1. 保证数据的不可变

通过immutable创建的对象在任何时候都无法改变

  1. 丰富的API

提供了丰富的API,Map,Set,List,Record,还有对应的操作,get,set,sort等等

  1. 优异的性能

再谈柯里化

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

也许就像这样

var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2)
//12
复制代码

这里我们定义了一个 add 函数,它接受一个参数并返回一个新的函数。调用 add 之后,返回的函数就通过闭包的方式记住了 add 的第一个参数。一次性地 调用它实在是有点繁琐,好在我们可以使用一个特殊的 curry 帮助函数使这类函数的定义和调用更加容易

currying完成的事情就是函数(接口)封装,它将一个已有的函数(接口)做封装,得到一个新的函数(接口), 这是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。 下面才是重点:

export default store => next => action => {
    return next(action)
}
复制代码

这就是redux中间件的实现,嵌套了三层函数,分别传递了store、next、action这三个参数,最后返回next(action)。

为何不在一层函数中同时传递三个参数呢?当然如果只为了传递store、next、action这三个参数我们直接可以写成一层,可是这里中间件的每一层函数将来都会 单独运行 ,所以利用curry函数 延迟执行 的特性,记忆住每一层函数的返回,形成三个单独函数。

我们再来解释一下为什么要这么麻烦延迟执行?首先先来看一下redux对中间件处理的applymiddleware的源码:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}
复制代码
  1. -中间件执行的第一层从这里开始:
var middlewareAPI = {
    getState: store.getState,
    dispatch: function dispatch(action) {
      return _dispatch(action);
    }
  };
 chain = middlewares.map(function (middleware) {
    return middleware(middlewareAPI);
  });
复制代码

这里生成一个中间件函数数组,并将middlewareAPI传入,这里其实就是中间件形成的第一步,将store传入,这里还有一个点就是在利用了闭包的原理,中间件的执行过程中若是有改变store的操作,会同步更新middlewareAPI,使得传入每个middleware的store都是最新的

  1. -第二层的next执行在这里:
_dispatch = _compose2['default'].apply(undefined, chain)(store.dispatch);
复制代码

compose是函数式编程中一个重要的功能:‘函数组合’,利用reduce函数将middleware组合成嵌套函数,最后结果是这样:_dispatch=mid1(mid2(mid3(...(store.dispathch)))),形成pipe(管道),对最原始的dispatch进行了一个功能的增强

3.-第三层的实现,大家应该都使用过,就像这样:触发一个action

dispatch(action)复制代码

总结:讲到这里已经将函数式编程中基础的概念以及他们在react中的常见应用已经讲完,当然函数式编程远不仅限于此,还有一些学院派叫人晦涩难懂的高级理论知识,大约就是pointfree,handley-milner类型签名,函子...准备写在下一篇文章中。

函数式编程是一种理念大于实践的编码理论知识,并不是所有的理念都适用于现在(比如js),要随着语言的不断发展,加深对函数式的渗透,他才能慢慢转理论为实践。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

设计原本

设计原本

Frederick P. Brooks, Jr. / InfoQ中文站、王海鹏、高博 / 机械工业出版社 / 2011-1-1 / 55.00元

无论是软件开发、工程还是建筑,有效的设计都是工作的核心。《设计原本:计算机科学巨匠Frederick P. Brooks的思考》将对设计过程进行深入分析,揭示进行有效和优雅设计的方法。 本书包含了多个行业设计者的特别领悟。Frederick P. Brooks, Jr.精确发现了所有设计项目中内在的不变因素,揭示 了进行优秀设计的过程和模式。通过与几十位优秀设计者的对话,以及他自己在几个设计......一起来看看 《设计原本》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试