React 中的函數式思想

題圖:React Illustration

函數式編程簡要概念

函數式編程中一個核心概念之一就是純函數,如果一個函數滿足一下幾個條件,就可以認為這個函數是純函數了:

  1. 它是一個函數(廢話);

  2. 當給定相同的輸入(函數的參數)的時候,總是有相同的輸出(返回值);

  3. 沒有副作用;

  4. 不依賴於函數外部狀態。

當一個函數滿足以上條件的時候,就可以認為這個函數是純函數了。舉個栗子:

// 非純函數let payload = 0;function addOne(number) { ++payload; return number + payload;}addOne(1); // 2addOne(1); // 3addOne(1); // 4// 純函數function addOne(number) { return number + 1;}addOne(1); // 2addOne(1); // 2addOne(1); // 2

上面兩個栗子中,第一個就是典型的非純函數,當第一次執行 `addOne(1)` 其返回的值是 `2` 沒有錯,但是再次執行相同函數的時候,其返回的值不再是 `2` 了,而是變成了 `3` ,對比上面列出的滿足純函數的條件,就會發現:

  • `addOne()` 給定相同的輸入的時候沒有返回相同的輸出;

  • `addOne()` 會產生副作用(會改變外部狀態 `payload` );
  • `addOne()` 依賴的外部狀態 `payload` 。

而第二個栗子就是一個純函數,它既不依賴外部狀態也不會產生副作用,且當給定相同輸入的時候,總是返回相同的輸出(執行任意多次 `addOne(1)` 總是返回 `2` )。

以上對純函數概念的一些簡單理解。

React 核心理念

官方給出的 React 的定義是:

A JavaScript library for building user interfaces.

即專註於構建 View 層的一個庫。React 的核心開發者之一的 Sebastian Markb?ge 認為:

UI 只是把數據通過映射關係變成另一種形式的數據。給定相同的輸入(數據)必然會有相同的輸出(UI),即一個簡單的純函數。

React 中的函數式思想的具體體現

雖說 View 層可以當成是數據的另外一種展現形式,但在實際的 React 開發中,除了數據的展示以外,更重要的是還有數據的交互,舉個栗子:

import React, { Component } from react;import { fetchPosts } from path/to/api;export default class PostList extends Component { constructor() { this.state = { posts: [], }; } componentDidMount() { fetchPosts().then(posts => { this.setState({ posts: posts, }); }); } render() { return ( <ul> { this.state.posts.map(post => { return ( <li key={post.id} onClick={this.toggleActive}>{ post.title }</li> ); }); } </ul> ); } toggleActive() { // }}

這個一個典型的渲染列表的栗子,在這個栗子中除了渲染 `PostList` 外,還進行了數據的獲取和事件的操作,也就意味著這個 `PostList` 組件不是一個」純函數「。嚴格意義上來說這個組件還不是一個可復用的組件,比如說有這樣一種業務場景,除了首頁有 `PostList` 組件以外,在個人頁面同樣有個 `PostList` 組件,UI 一致但是交互邏輯不一致,這種情況下就無法復用首頁的 `PostList` 組件了。為了解決這個問題,我們可以再次抽離一個真正意義上可復用的 View 層,它有一下幾個特點:

  • 給定相同的數據(由父組件通過 props 傳遞給子組件且是唯一數據來源),總是渲染相同的 UI 界面;

  • 組件你內部不改變數據狀態;

  • 不處理交互邏輯。

可以發現,這個上面所列出的滿足純函數的條件非常相似,這種組件才算是真正意義上的可復用的組件,好了,Talk is cheap, show me the code:

import React, { Component } from react;import { fetchPosts } from path/to/api;export default class PostListContainer extends Component { constructor() { this.state = { posts: [], }; } componentDidMount() { fetchPosts().then(posts => { this.setState({ posts: posts, }); }); } render() { return ( <PostList posts={this.state.posts} toggleActive={this.toggleActive}></PostList> ); } toggleActive() { // }}//export default class PostList extends Component { render() { return (<ul>{ this.props.posts.map(this.renderPost); }</ul>); } renderPost(post) { return (<li key={post.id} onClick={this.props.toggleActive}>{ post.title }</li>); }}

通過這樣改造之後,原本數據交互和 UI 展示耦合則組件就被分為了兩個職責明確的新組建,即 `PostListContainer` 負責數據獲取或點擊等交互邏輯,而 `PostList` 則真正意義上的只負責純粹的 View 層渲染。這種情況下的 `PostListContainer` 被稱為 Container Component(容器組件),`PostList` 則被稱為 Presentational Container(展示組件)。再回到剛剛所假設的業務場景下,此時可以通過創建不同的 Container Component 來處理不同的交互邏輯,然後把最終的數據通過 `props` 傳遞給子組件 `PostList`,這樣的話不管是首頁還是個人都可以真正復用 `PostList` 這個 Presentational Component 了。

再回過頭來思考一下前面提到的 Sebastian Markb?ge 所認為的理念:

UI 只是把數據通過映射關係變成另一種形式的數據。給定相同的輸入(數據)必然會有相同的輸出(UI),即一個簡單的純函數。

我們可以把這句話高度抽象成一個函數:`data => View`,拿前面的 Presentational Component `PostList` 來說,其中 `this.props.posts` 就是 `data => View` 中的 `data`,而整個渲染結果就是 `View`,我們再單獨分析一下這個組件:

import React, { Component } from react;export default class PostList extends Component { render() { return (<ul>{ this.props.posts.map(this.renderPost); }</ul>); } renderPost(post) { return (<li key={post.id} onClick={this.props.toggleActive}>{ post.title }</li>); }}

其實會發現,儘管這個組件已經很簡單了,`this.props.posts` 傳入數據,然後渲染結果(同時還有綁定事件,但是沒有事件處理的具體邏輯),沒有再做其他操作了。但我們仔細思考的話,還是會發現有兩個比較明顯的問題,一個是寫法上還是典型的面向對象的方式來寫的;其次是該組件內部還有 `this` 關鍵字,為什麼說在這裡使用關鍵字 `this` 是不合適的呢,因為 JavaScript 嚴格來說並不是函數式編程語言,在 JavaScript 中 `this` 的指向又非常容易的被改變,所以依賴於 `this` 關鍵字的 data 是非常不穩定的。

好在以上兩個問題再 React 的 v0.14 版本中得到了解決,在此次版本中 React 有一個新的特性叫 Stateless Functional Components。什麼意思呢?我們把上面的 `PostList` 組件以 Stateless Functional Components 的方式來重新編寫就會一目了然了:

let PostList = props => ( <ul> { props.posts.map(post => (<li key={ post.id } onClick={ props.toggleActive }>{ post.title }</li>)); } </ul>);// 參數解構let PostList = ({ posts, toggleActive }) => ( <ul> { posts.map(post => (<li key={ post.id } onClick={ toggleActive }>{ post.title }</li>)); } </ul>);

我們會發現 Stateless Functional Components 完美的詮釋了前面所提到的 `data => View` 這個理念,不僅數據輸入不依賴於 `this` 關鍵字,且書寫風格也更像函數式風格。

總結

在平時的開發中,應該避免數據交互邏輯與數據渲染的過於耦合,嚴格區分 Container Component 和 Presentational Component 的職責不僅可以更容易的復用組件,而且也容易定位問題的所在。

參考文章:

1. Which Programming Languages Are Functional?


推薦閱讀:

如何看待基於 Atom 和 Lens 的狀態管理工具 Calmm-js 和 Focal?

TAG:React | 函數式編程 | FunctionalReactiveProgramming |