React + Ramda: 函數式編程嚐鮮

原文:Functional Components with React stateless functions and Ramdanode

閱讀本文須要的知識儲備:react

  • 函數式編程基本概念(組合、柯里化、透鏡)
  • React 基本知識(組件、狀態、屬性、JSX)
  • ES6 基本知識(class、箭頭函數)

React 無狀態函數

React 組件最多見的定義方法:git

const  List = React.createClass({
  render: function() {
    return (<ul>{this.props.children}</ul>);
  }
});

或者使用 ES6 類語法:github

class List extends React.Component {
  render() {
    return (<ul>{this.props.children}</ul>);
  }
}

又或者使用普通的 JS 函數:編程

// 無狀態函數語法
const List = function(children) {
  return (<ul>{children}</ul>);
};

//ES6 箭頭函數語法
const List = (children) => (<ul>{children}</ul>);

React 官方文檔對這種組件作了如下說明:數組

這種簡化的組件 API 適用於僅依賴屬性的純函數組件。這些組件不容許擁有內部狀態,不會生成組件實例,也沒有組件的生命週期方法。它們只對輸入進行純函數轉換。不過開發者仍然能夠爲它們指定 .propTypes.defaultProps,只須要設置爲函數的屬性就能夠了,就跟在 ES6 類上設置同樣。

同時也說到:性能優化

理想狀況下,大部分的組件都應該是無狀態的函數,由於在將來咱們可能會針對這類組件作性能優化,避免沒必要要的檢查和內存分配。因此推薦你們儘量的使用這種模式來開發。

是否是以爲挺有趣的?app

React 社區彷佛更加關注經過 classcreateClass 方式來建立組件,今天讓咱們來嚐鮮一下無狀態組件。less

App 容器

首先讓咱們來建立一個函數式 App 容器組件,它接受一個表示應用狀態的對象做爲參數:dom

import React from 'react';
import ReactDOM from 'react-dom';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

而後,定義一個 render 方法,做爲 App 函數的屬性:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <p>Some children here...</p>
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

等等!有點看不明白了!
爲何咱們須要一個柯里化的渲染函數?又爲何渲染函數的參數順序反過來了?
別急別急,這裏惟一要說明的是,因爲咱們使用的是無狀態組件,因此狀態必須由其它地方來維護。也就是說,狀態必須由外部維護,而後經過屬性的方式傳遞給組件。
讓咱們來看一個具體的計時器例子。

無狀態計時器組件

一個簡單的計時器組件只接受一個屬性 secondsElapsed

import React from 'react';

export default ({ secondsElapsed }) => (<div className="well">
  Seconds Elapsed: {secondsElapsed}
</div>);

把它添加到 App 中:

import React from 'react';
import ReactDOM from 'react-dom';
import R from 'ramda';
import Timer from './timer';

const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
</div>);

App.render = R.curry((node, props) => ReactDOM.render(<App {...props}/>, node));

export default App;

最後,建立 main.js 來渲染 App

import App from './components/app';
const render = App.render(document.getElementById('app'));

let appState = {
  secondsElapsed: 0
};

//first render
render(appState);

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

在進一步說明以前,我想說,appState.secondElapsed++ 這種修改狀態的方式讓我以爲很是不爽,不過稍後咱們會使用更好的方式來實現。

這裏咱們能夠看出,render 其實就是用新屬性來從新渲染組件的語法糖。下面這行代碼:

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

會返回一個具備 (props) => ReactDOM.render(...) 函數簽名的函數。
這裏並無什麼太難理解的內容。每當 secondsElapsed 的值改變後,咱們只須要從新調用 render 方法便可:

setInterval(() => {
  appState.secondsElapsed++;
  render(appState);
}, 1000);

如今,讓咱們來實現一個相似 Redux 風格的歸約函數,以不斷的遞增 secondsElapsed。歸約函數是不容許修改當前狀態的,全部最簡單的實現方式就是 currentState -> newState

這裏咱們使用 Ramda 的透鏡(Lens)來實現 incSecondsElapsed 函數:

const secondsElapsedLens = R.lensProp('secondsElapsed');
const incSecondsElapsed = R.over(secondsElapsedLens, R.inc);

setInterval(() => {
  appState = incSecondsElapsed(appState);
  render(appState);
}, 1000);

第一行代碼中,咱們建立了一個透鏡:

const secondsElapsedLens = R.lensProp('secondsElapsed');

簡單來講,透鏡是一種專一於給定屬性的方式,而不關心該屬性究竟是在哪一個對象上,這種方式便於代碼複用。當咱們須要把透鏡應用於對象上時,能夠有如下操做:

  • View
R.view(secondsElapsedLens, { secondsElapsed: 10 });  //=> 10
  • Set
R.set(secondsElapsedLens, 11, { secondsElapsed: 10 });  //=> 11
  • 以給定函數來設置
R.over(secondsElapsedLens, R.inc, { secondsElapsed: 10 });  //=> 11

咱們實現的 incSecondsElapsed 就是對 R.over 進行局部應用的結果。

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

該行代碼會返回一個新函數,一旦調用時傳入 appState,就會把 R.inc 應用在 secondsElapsed 屬性上。

須要注意的是,Ramda 歷來都不會修改對象,因此咱們須要本身來處理髒活:

appState = incSecondsElapsed(appState);

若是想支持 undo/redo ,只須要維護一個歷史數組記錄下每一次狀態便可,或者使用 Redux 。

目前爲止,咱們已經品嚐了柯里化和透鏡,下面讓咱們繼續品嚐組合

組合 React 無狀態組件

當我第一次讀到 React 無狀態組件時,我就在想可否使用 R.compose 來組合這些函數呢?答案很明顯,固然是 YES 啦:)

讓咱們從一個 TodoList 組件開始:

const TodoList = React.createClass({
  render: function() {
    const createItem = function(item) {
      return (<li key={item.id}>{item.text}</li>);
    };

    return (<div className="panel panel-default">
      <div className="panel-body">
        <ul>
          {this.props.items.map(createItem)}
        </ul>
      </div>
    </div>);
  }
});

如今問題來了,TodoList 可否經過組合更小的、可複用的組件來實現呢?固然,咱們能夠把它分割成 3 個小組件:

  • 容器
const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);
  • 列表
const List = children => (<ul>
  {children}
</ul>);
  • 列表項
const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

如今,咱們來一步一步看,請必定要在理解了每一步以後才往下看:

Container(<h1>Hello World!</h1>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <h1>Hello World!</h1>
 *    </div>
 *  </div>
 */

Container(List(<li>Hello World!</li>));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
Container(List(ListItem(TodoItem)));

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

沒有什麼太特別的,只不過是一步一步的傳參調用。

接着,讓咱們來作一些組合的練習:

R.compose(Container, List)(<li>Hello World!</li>);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>Hello World!</li>
 *      </ul>
 *    </div>
 *  </div>
 */

const ContainerWithList = R.compose(Container, List);
R.compose(ContainerWithList, ListItem)({id: 123, text: 'Buy milk'});

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

const TodoItem = {
  id: 123,
  text: 'Buy milk'
};
const TodoList = R.compose(Container, List, ListItem);
TodoList(TodoItem);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>Buy milk</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

發現了沒!TodoList 組件已經被表示成了 ContainerListListItem 的組合了:

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

等等!TodoList 這個組件只接受一個 todo 對象,可是咱們須要的是映射整個 todos 數組:

const mapTodos = function(todos) {
  return todos.map(function(todo) {
    return ListItem(todo);
  });
};
const TodoList = R.compose(Container, List, mapTodos);
const mock = [
  {id: 1, text: 'One'},
  {id: 1, text: 'Two'},
  {id: 1, text: 'Three'}
];
TodoList(mock);

/**
 *  <div className="panel panel-default">
 *    <div className="panel-body">
 *      <ul>
 *        <li>
 *          <span>One</span>
 *        </li>
 *        <li>
 *          <span>Two</span>
 *        </li>
 *        <li>
 *          <span>Three</span>
 *        </li>
 *      </ul>
 *    </div>
 *  </div>
 */

可否以更函數式的方式簡化 mapTodos 函數?

// 下面的代碼
return todos.map(function(todo) {
  return ListItem(todo);
});

// 等效於
return todos.map(ListItem);

// 因此變成了
const mapTodos = function(todos) {
  return todos.map(ListItem);
};

// 等效於使用 Ramda 的方式
const mapTodos = function(todos) {
  return R.map(ListItem, todos);
};

// 注意 Ramda 的兩個特色:
// - Ramda 函數默認都支持柯里化
// - 爲了便於柯里化,Ramda 函數的參數進行了特定排列,
//   待處理的數據一般放在最後
// 所以:
const mapTodos = R.map(ListItem);

//此時就再也不須要 mapTodos 了:
const TodoList = R.compose(Container, List, R.map(ListItem));

噠噠噠!完整的 TodoList 實現代碼以下:

import React from 'React';
import R from 'ramda';

const Container = children => (<div className="panel panel-default">
  <div className="panel-body">
    {children}
  </div>
</div>);

const List = children => (<ul>
  {children}
</ul>);

const ListItem = ({ id, text }) => (<li key={id}>
  <span>{text}</span>
</li>);

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

export default TodoList;

其實,還少了同樣東西,不過立刻就會加上。在那以前讓咱們先來作些準備:

  • 添加測試數據到應用狀態
let appState = {
  secondsElapsed: 0,
  todos: [
    {id: 1, text: 'Buy milk'},
    {id: 2, text: 'Go running'},
    {id: 3, text: 'Rest'}
  ]
};
  • 添加 TodoListApp
import TodoList from './todo-list';
const App = appState => (<div className="container">
  <h1>App name</h1>
  <Timer secondsElapsed={appState.secondsElapsed} />
  <TodoList todos={appState.todos} />
</div>);

TodoList 接受的是一個 todos 數組,可是這裏倒是:

<TodoList todos={appState.todos} />

咱們把列表傳遞做爲一個屬性,因此等效於:

TodoList({todos: appState.todos});

所以,咱們必須修改 TodoList,以便讓它接受一個對象而且取出 todos 屬性:

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

這裏並無什麼高深技術。僅僅是從右到左的組合,R.prop('todos') 會返回一個函數,調用該函數會返回其做爲的參數對象的 todos 屬性,接着把該屬性值傳遞給 R.map(ListItem),如此往復:)

以上就是本文的嚐鮮內容。但願能對你們有所幫助,這僅僅是我基於 React 和 Ramda 作的一部分實驗。將來,我會努力嘗試覆蓋高階組件和使用 Transducer 來轉換無狀態函數。

完整源碼線上演示代碼(譯者新增)。

相關文章
相關標籤/搜索