1. 程式人生 > >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 (
            <ul>
                { this.state.posts.map(post => <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,
            });
        });
    }
    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(post => <li key={post.id} onClick={this.props.toggleActive}>{ post.title }</li>) }</ul>);
    }
}

通過這樣改造之後,原本資料互動和 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(post => <li key={post.id} onClick={this.props.toggleActive}>{ post.title }</li>) }</ul>);
    }
}

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

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


const PostList = props => (
    <ul>
        { props.posts.map(post => (<li key={ post.id } onClick={ props.toggleActive }>{ post.title }</li>)) }
    </ul>
);

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

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

總結

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

參考文章:

  1. Which Programming Languages Are Functional?

來源:https://segmentfault.com/a/1190000017750929