React 中的函數式思想

React 中的函數式思想

函數式編程簡要概念

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

  • 它是一個函數(廢話);
  • 當給定相同的輸入(函數的參數)的時候,總是有相同的輸出(返回值);
  • 沒有副作用;
  • 不依賴於函數外部狀態。

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

// 非純函數
let payload = 0;
function addOne(number) {
++payload;
return number + payload;
}
addOne(1); // 2
addOne(1); // 3
addOne(1); // 4
// 純函數
function addOne(number) {
return number + 1;
}
addOne(1); // 2
addOne(1); // 2
addOne(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 (

    { this.state.posts.map(post =>
  • { post.title }
  • ) }

);
}
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,
});
});
}
render() {
return (
<postlist>
);
}
toggleActive() {
//
}
}
//
export default class PostList extends Component {
render() {
return (
    { this.props.posts.map(post =>
  • { post.title }
  • ) }
);
}
}

通過這樣改造之後,原本數據交互和 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 (
    { this.props.posts.map(post =>
  • { post.title }
  • ) }
);
}
}

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

React 中的函數式思想

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

const PostList = props => (

    { props.posts.map(post => (
  • { post.title }
  • )) }

);
// 參數解構
const PostList = ({ posts, toggleActive }) => (

    { posts.map(post => (
  • { post.title }
  • )) }

);

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

總結

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


分享到:


相關文章: