资讯专栏INFORMATION COLUMN

React + Ramda: 函数式编程尝鲜

tomener / 3564人阅读

摘要:每当的值改变后,我们只需要重新调用方法即可现在,让我们来实现一个类似风格的归约函数,以不断的递增。归约函数是不允许修改当前状态的,所有最简单的实现方式就是。

原文:Functional Components with React stateless functions and Ramda

阅读本文需要的知识储备:

函数式编程基本概念(组合、柯里化、透镜)

React 基本知识(组件、状态、属性、JSX)

ES6 基本知识(class、箭头函数)

React 无状态函数

React 组件最常见的定义方法:

</>复制代码

  1. const List = React.createClass({
  2. render: function() {
  3. return (
      {this.props.children}
    );
  4. }
  5. });

或者使用 ES6 类语法:

</>复制代码

  1. class List extends React.Component {
  2. render() {
  3. return (
      {this.props.children}
    );
  4. }
  5. }

又或者使用普通的 JS 函数:

</>复制代码

  1. // 无状态函数语法
  2. const List = function(children) {
  3. return (
      {children}
    );
  4. };
  5. //ES6 箭头函数语法
  6. const List = (children) => (
      {children}
    );

React 官方文档对这种组件做了以下说明:

</>复制代码

  1. 这种简化的组件 API 适用于仅依赖属性的纯函数组件。这些组件不允许拥有内部状态,不会生成组件实例,也没有组件的生命周期方法。它们只对输入进行纯函数转换。不过开发者仍然可以为它们指定 .propTypes.defaultProps,只需要设置为函数的属性就可以了,就跟在 ES6 类上设置一样。

同时也说到:

</>复制代码

  1. 理想情况下,大部分的组件都应该是无状态的函数,因为在未来我们可能会针对这类组件做性能优化,避免不必要的检查和内存分配。所以推荐大家尽可能的使用这种模式来开发。

是不是觉得挺有趣的?

React 社区似乎更加关注通过 classcreateClass 方式来创建组件,今天让我们来尝鲜一下无状态组件。

App 容器

首先让我们来创建一个函数式 App 容器组件,它接受一个表示应用状态的对象作为参数:

</>复制代码

  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. const App = appState => (
  4. App name

  5. Some children here...

  6. );

然后,定义一个 render 方法,作为 App 函数的属性:

</>复制代码

  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. import R from "ramda";
  4. const App = appState => (
  5. App name

  6. Some children here...

  7. );
  8. App.render = R.curry((node, props) => ReactDOM.render(, node));
  9. export default App;

等等!有点看不明白了!
为什么我们需要一个柯里化的渲染函数?又为什么渲染函数的参数顺序反过来了?
别急别急,这里唯一要说明的是,由于我们使用的是无状态组件,所以状态必须由其它地方来维护。也就是说,状态必须由外部维护,然后通过属性的方式传递给组件。
让我们来看一个具体的计时器例子。

无状态计时器组件

一个简单的计时器组件只接受一个属性 secondsElapsed

</>复制代码

  1. import React from "react";
  2. export default ({ secondsElapsed }) => (
  3. Seconds Elapsed: {secondsElapsed}
  4. );

把它添加到 App 中:

</>复制代码

  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. import R from "ramda";
  4. import Timer from "./timer";
  5. const App = appState => (
  6. App name

  7. );
  8. App.render = R.curry((node, props) => ReactDOM.render(, node));
  9. export default App;

最后,创建 main.js 来渲染 App

</>复制代码

  1. import App from "./components/app";
  2. const render = App.render(document.getElementById("app"));
  3. let appState = {
  4. secondsElapsed: 0
  5. };
  6. //first render
  7. render(appState);
  8. setInterval(() => {
  9. appState.secondsElapsed++;
  10. render(appState);
  11. }, 1000);

在进一步说明之前,我想说,appState.secondElapsed++ 这种修改状态的方式让我觉得非常不爽,不过稍后我们会使用更好的方式来实现。

这里我们可以看出,render 其实就是用新属性来重新渲染组件的语法糖。下面这行代码:

</>复制代码

  1. const render = App.render(document.getElementById(‘app’));

会返回一个具有 (props) => ReactDOM.render(...) 函数签名的函数。
这里并没有什么太难理解的内容。每当 secondsElapsed 的值改变后,我们只需要重新调用 render 方法即可:

</>复制代码

  1. setInterval(() => {
  2. appState.secondsElapsed++;
  3. render(appState);
  4. }, 1000);

现在,让我们来实现一个类似 Redux 风格的归约函数,以不断的递增 secondsElapsed。归约函数是不允许修改当前状态的,所有最简单的实现方式就是 currentState -> newState

这里我们使用 Ramda 的透镜(Lens)来实现 incSecondsElapsed 函数:

</>复制代码

  1. const secondsElapsedLens = R.lensProp("secondsElapsed");
  2. const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);
  3. setInterval(() => {
  4. appState = incSecondsElapsed(appState);
  5. render(appState);
  6. }, 1000);

第一行代码中,我们创建了一个透镜:

</>复制代码

  1. const secondsElapsedLens = R.lensProp("secondsElapsed");

简单来说,透镜是一种专注于给定属性的方式,而不关心该属性到底是在哪个对象上,这种方式便于代码复用。当我们需要把透镜应用于对象上时,可以有以下操作:

View

</>复制代码

  1. R.view(secondsElapsedLens, { secondsElapsed: 10 }); //=> 10

Set

</>复制代码

  1. R.set(secondsElapsedLens, 11, { secondsElapsed: 10 }); //=> 11

以给定函数来设置

</>复制代码

  1. R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 }); //=> 11

我们实现的 incSecondsElapsed 就是对 R.over 进行局部应用的结果。

</>复制代码

  1. const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

该行代码会返回一个新函数,一旦调用时传入 appState,就会把 R.inc 应用在 secondsElapsed 属性上。

需要注意的是,Ramda 从来都不会修改对象,所以我们需要自己来处理脏活:

</>复制代码

  1. appState = incSecondsElapsed(appState);

如果想支持 undo/redo ,只需要维护一个历史数组记录下每一次状态即可,或者使用 Redux 。

目前为止,我们已经品尝了柯里化和透镜,下面让我们继续品尝组合

组合 React 无状态组件

当我第一次读到 React 无状态组件时,我就在想能否使用 R.compose 来组合这些函数呢?答案很明显,当然是 YES 啦:)

让我们从一个 TodoList 组件开始:

</>复制代码

  1. const TodoList = React.createClass({
  2. render: function() {
  3. const createItem = function(item) {
  4. return (
  5. {item.text}
  6. );
  7. };
  8. return (
    • {this.props.items.map(createItem)}
  9. );
  10. }
  11. });

现在问题来了,TodoList 能否通过组合更小的、可复用的组件来实现呢?当然,我们可以把它分割成 3 个小组件:

容器

</>复制代码

  1. const Container = children => (
  2. {children}
  3. );

列表

</>复制代码

  1. const List = children => (
    • {children}
    );

列表项

</>复制代码

  1. const ListItem = ({ id, text }) => (
  2. {text}
  3. );

现在,我们来一步一步看,请一定要在理解了每一步之后才往下看:

</>复制代码

  1. Container(

    Hello World!

    );
  2. /**
  3. *
  4. *
  5. *

    Hello World!

  6. *
  7. *
  8. */
  9. Container(List(
  10. Hello World!
  11. ));
  12. /**
  13. *
  14. *
  15. *
    • *
    • Hello World!
    • *
  16. *
  17. *
  18. */
  19. const TodoItem = {
  20. id: 123,
  21. text: "Buy milk"
  22. };
  23. Container(List(ListItem(TodoItem)));
  24. /**
  25. *
  26. *
  27. *
    • *
    • * Buy milk
    • *
    • *
  28. *
  29. *
  30. */

没有什么太特别的,只不过是一步一步的传参调用。

接着,让我们来做一些组合的练习:

</>复制代码

  1. R.compose(Container, List)(
  2. Hello World!
  3. );
  4. /**
  5. *
  6. *
  7. *
    • *
    • Hello World!
    • *
  8. *
  9. *
  10. */
  11. const ContainerWithList = R.compose(Container, List);
  12. R.compose(ContainerWithList, ListItem)({id: 123, text: "Buy milk"});
  13. /**
  14. *
  15. *
  16. *
    • *
    • * Buy milk
    • *
    • *
  17. *
  18. *
  19. */
  20. const TodoItem = {
  21. id: 123,
  22. text: "Buy milk"
  23. };
  24. const TodoList = R.compose(Container, List, ListItem);
  25. TodoList(TodoItem);
  26. /**
  27. *
  28. *
  29. *
    • *
    • * Buy milk
    • *
    • *
  30. *
  31. *
  32. */

发现了没!TodoList 组件已经被表示成了 ContainerListListItem 的组合了:

</>复制代码

  1. const TodoList = R.compose(Container, List, ListItem);

等等!TodoList 这个组件只接受一个 todo 对象,但是我们需要的是映射整个 todos 数组:

</>复制代码

  1. const mapTodos = function(todos) {
  2. return todos.map(function(todo) {
  3. return ListItem(todo);
  4. });
  5. };
  6. const TodoList = R.compose(Container, List, mapTodos);
  7. const mock = [
  8. {id: 1, text: "One"},
  9. {id: 1, text: "Two"},
  10. {id: 1, text: "Three"}
  11. ];
  12. TodoList(mock);
  13. /**
  14. *
  15. *
  16. *
    • *
    • * One
    • *
    • *
    • * Two
    • *
    • *
    • * Three
    • *
    • *
  17. *
  18. *
  19. */

能否以更函数式的方式简化 mapTodos 函数?

</>复制代码

  1. // 下面的代码
  2. return todos.map(function(todo) {
  3. return ListItem(todo);
  4. });
  5. // 等效于
  6. return todos.map(ListItem);
  7. // 所以变成了
  8. const mapTodos = function(todos) {
  9. return todos.map(ListItem);
  10. };
  11. // 等效于使用 Ramda 的方式
  12. const mapTodos = function(todos) {
  13. return R.map(ListItem, todos);
  14. };
  15. // 注意 Ramda 的两个特点:
  16. // - Ramda 函数默认都支持柯里化
  17. // - 为了便于柯里化,Ramda 函数的参数进行了特定排列,
  18. // 待处理的数据通常放在最后
  19. // 因此:
  20. const mapTodos = R.map(ListItem);
  21. //此时就不再需要 mapTodos 了:
  22. const TodoList = R.compose(Container, List, R.map(ListItem));

哒哒哒!完整的 TodoList 实现代码如下:

</>复制代码

  1. import React from "React";
  2. import R from "ramda";
  3. const Container = children => (
  4. {children}
  5. );
  6. const List = children => (
    • {children}
    );
  7. const ListItem = ({ id, text }) => (
  8. {text}
  9. );
  10. const TodoList = R.compose(Container, List, R.map(ListItem));
  11. export default TodoList;

其实,还少了一样东西,不过马上就会加上。在那之前让我们先来做些准备:

添加测试数据到应用状态

</>复制代码

  1. let appState = {
  2. secondsElapsed: 0,
  3. todos: [
  4. {id: 1, text: "Buy milk"},
  5. {id: 2, text: "Go running"},
  6. {id: 3, text: "Rest"}
  7. ]
  8. };

添加 TodoListApp

</>复制代码

  1. import TodoList from "./todo-list";
  2. const App = appState => (
  3. App name

  4. );

TodoList 接受的是一个 todos 数组,但是这里却是:

</>复制代码

我们把列表传递作为一个属性,所以等效于:

</>复制代码

  1. TodoList({todos: appState.todos});

因此,我们必须修改 TodoList,以便让它接受一个对象并且取出 todos 属性:

</>复制代码

  1. const TodoList = R.compose(Container, List, R.map(ListItem), R.prop("todos"));

这里并没有什么高深技术。仅仅是从右到左的组合,R.prop("todos") 会返回一个函数,调用该函数会返回其作为的参数对象的 todos 属性,接着把该属性值传递给 R.map(ListItem),如此往复:)

以上就是本文的尝鲜内容。希望能对大家有所帮助,这仅仅是我基于 React 和 Ramda 做的一部分实验。未来,我会努力尝试覆盖高阶组件和使用 Transducer 来转换无状态函数。

完整源码,线上演示代码(译者新增)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96005.html

相关文章

  • 如何优雅安全地在深层数据结构中取值

    摘要:如果这个结构非常复杂,那么想要安全优雅地取出一个值,也并非简单。这是为了在对象中相关取值的过程,需要验证每一个和的存在性。并且这个数据结构必然是动态生成的,存在有时有时的情况。在测试过程中,很难复现。 古有赵子龙面对冲锋之势,有进无退,陷阵之志,有死无生的局面,能万军丛中取敌将首级。在我们的Javascript中,往往用对象(Object)来存储一个数据结构。如果这个结构非常复杂,那么...

    RobinQu 评论0 收藏0
  • 如何优雅安全地在深层数据结构中取值

    摘要:如果这个结构非常复杂,那么想要安全优雅地取出一个值,也并非简单。这是为了在对象中相关取值的过程,需要验证每一个和的存在性。并且这个数据结构必然是动态生成的,存在有时有时的情况。在测试过程中,很难复现。 古有赵子龙面对冲锋之势,有进无退,陷阵之志,有死无生的局面,能万军丛中取敌将首级。在我们的Javascript中,往往用对象(Object)来存储一个数据结构。如果这个结构非常复杂,那么...

    liaorio 评论0 收藏0
  • 翻译连载 | 附录 C:函数编程函数库-《JavaScript轻量级函数编程》 |《你不知道的J

    摘要:为了尽可能提升互通性,已经成为函数式编程库遵循的实际标准。与轻量级函数式编程的概念相反,它以火力全开的姿态进军的函数式编程世界。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,...

    Miracle 评论0 收藏0
  • 前端常用插件、工具类库汇总

    摘要:页面调试腾讯开发维护的代码调试发布,错误监控上报,用户问题定位。同样是由腾讯开发维护的代码调试工具,是针对移动端的调试工具。前端业务代码工具库。动画库动画库,也是目前通用的动画库。 本人微信公众号:前端修炼之路,欢迎关注 本篇文章整理自己使用过的和看到过的一些插件和工具,方便日后自己查找和使用。 另外,感谢白小明,文中很多的工具来源于此。 弹出框 layer:http://layer....

    GitCafe 评论0 收藏0

发表评论

0条评论

tomener

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<