使用React的函數語言程式設計的基礎知識

語言: CN / TW / HK

理解函數語言程式設計的概念是React開發者的一項寶貴技能。這是一個重要的話題,大多數React初學者經常忽略,使他們在理解React如何做出一些決定時遇到了問題。

在這篇文章中,我們將介紹函數語言程式設計的概念以及React如何採用它來編寫更容易測試和維護的應用程式。

要學習本教程,請確保你對React有基本的瞭解。

函數語言程式設計的快速概述

我們編寫的每個程式或應用程式都遵循一種方法或寫作風格,也就是所謂的正規化。因此,函數語言程式設計是一種宣告式的程式設計範式,程式是由純函式組成的。

讓我們注意一下 "組合 "和 "純 "這兩個詞,因為它們構成了函數語言程式設計的基石,我們將在下面的章節中討論它們。

數學中的函式

為了更好地理解函數語言程式設計的概念,讓我們快速瀏覽一下數學中常見的函式。例如,我們有一個給定的函式。

``` y = f(x)

```

在這個函式中,輸出,y ,只對輸入,x 進行計算。這意味著每次我們用相同的輸入x ,呼叫這個函式時,我們總是得到相同的輸出,y

該函式不影響自身以外的任何東西,也不修改傳入的輸入。因此,它被稱為一個確定性的或純函式

讓我們看一個例子。

``` y = f(x) = 4x // if x = 2, y = 4(2) = 8

```

如上所見,對於每一個輸入,x = 2 ,輸出y ,將永遠是8 。像這樣的函式總是更容易理解和推理,因為我們確切地知道我們所期望的東西。

讓我們再往前走一步,寫一個更復雜的這樣的函式。

``` z = c(f(x))

```

在這裡,我們有兩個函式,cf ,它們被組合在一起形成一個更復雜的函式。在數學中,我們說cf(x) 的一個函式,這意味著我們必須首先單獨評估f(x) ,像這樣。

``` y = f(x)

```

然後,我們將結果作為引數傳遞給c ,像這樣。

``` z = c(y)

```

這種功能概念被稱為函式組合。它鼓勵程式碼的可重用性和可維護性。

有了這個數學概念,理解電腦科學程式設計中的函式式概念就是小菜一碟了。

React中的函數語言程式設計

接下來,我們將解釋函數語言程式設計概念是如何在React中應用的。我們還將看到JavaScript中的例子,因為它是React的底層語言。

讓我們先看一下下面的JavaScript程式碼。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { arr.push(ele); }

addElement(arr, 8);

console.log("original data", arr); // Expected Output: [2, 4, 6, 8]

```

在這裡,我們定義了一個名為addElement 的函式,向一個數組新增一個元素。該程式碼的工作原理如輸出所示,你可以 在CodeSandbox上 自己嘗試一下

這段程式碼看起來與前面解釋的數學中的函式概念相似。也就是說,一個函式只對輸入引數進行操作,以建立一個輸出。

但仔細觀察這段程式碼,我們會發現它違反了一個純粹的函式概念,即一個函式不應影響它以外的任何東西,也不應修改傳入的引數。

一個這樣做的函式是一個不純的函式,並且有副作用,例如操縱輸入引數,在本例中,全域性arr 陣列。

在程式碼中,全域性arr 陣列被突變了,也就是說,函式將全域性變數從最初的[2,4,6] 改為[2,4,6,8]

現在,想象一下我們想重新使用這個全域性變數來組成另一個函式。如果我們繼續這樣做,我們會得到一個非預期的輸出,可能會導致我們的程式出現錯誤。

這就把我們帶到了函數語言程式設計的第一個信條:純函式。

使用純函式

在函數語言程式設計中,我們編寫純函式,也就是隻在輸入值上返回輸出的函式,從不影響它們之外的任何東西。這有助於防止我們的程式碼中出現錯誤。

將這個函式式概念應用到上面的程式碼中,我們可以得到以下結果。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { return [...arr, ele]; }

console.log("modified data", addElement(arr, 8)); // Expected Output: [2, 4, 6, 8] console.log("original data", arr); // Expected Output: [2, 4, 6]

```

上面的函式是純粹的,它只在輸入引數上計算輸出,從不改變全域性arr ,我們可以從結果中看到。

你可以在CodeSandbox上自己嘗試一下

請注意,這段程式碼也使用了不變性的概念來使函式變得純粹(我們稍後會討論這個問題)。

這種型別的函式是可預測的,更容易測試,因為我們總是會得到預期的輸出。

React如何實現純函式的概念

一個React應用元件的最簡單形式是這樣的。

`` const Counter = ({ count }) => { return <h3>{Count: ${count}`}; };

```

這類似於JavaScript中的純函式,其中一個函式接收一個引數(在這種情況下,一個count 道具),並使用該道具來渲染輸出。

然而,React的資料流是單向的,從父元件到子元件。當狀態資料傳遞給子元件時,它就成為一個不可變的道具,不能被接收元件修改。

因此,鑑於相同的道具,這種型別的元件總是渲染相同的JSX。而且,正因為如此,我們可以在任何頁面部分重複使用該元件,而不用擔心不確定性。這種型別的元件是一個純粹的功能元件。

提高應用程式的效能

React利用純功能的概念來提高應用程式的效能。由於React的特性,每當一個元件的狀態發生變化時,React都會重新渲染該元件及其子元件,即使狀態變化並不直接影響子元件。

在這種情況下,React允許我們用React.memo ,以防止不必要的重新渲染,如果它收到的道具從未改變。

通過對上述純函式進行備忘,我們只在count 道具發生變化時才重新渲染該函式。

`` const CounterComponent = React.memo(function Counter({ count }) { return <h3>{Count: ${count}`}; });

```

狀態更新中的純功能概念

React在更新狀態變數時也實現了函式式的概念,特別是當一個值是基於前一個值的時候,就像在一個計數器或複選框的情況下。

看一下下面的程式碼。

``` import { useState } from "react"; const App = () => { const [count, setCount] = useState(0);

const handleClick = () => setCount(count + 1);

return ( // ... ); };

const Counter = ({ count }) => { // ... };

export default App;

```

在這裡,為了簡潔起見,我們刪除了部分程式碼,但擴充套件了我們之前的Counter 元件,以顯示一個按鈕來增加一個計數

對於React初學者來說,這是一段熟悉的程式碼。雖然這段程式碼可以使用,但我們可以通過遵循函數語言程式設計的概念來增加改進。

讓我們專注於程式碼的這一部分。

``` const handleClick = () => setCount(count + 1);

```

setCount 更新器函式裡面,我們使用了一個count 變數,不作為引數傳遞。正如我們所瞭解的,這違背了函數語言程式設計的概念。

React提供的一個改進是向updater函式傳遞一個回撥。在這個回撥中,我們可以訪問狀態的上一個版本,從那裡,我們可以更新狀態值。

``` const handleClick = () => setCount((prev) => prev + 1);

```

正如我們在setCount 回撥中所看到的,輸出只在prev 輸入引數上進行計算。這就是純函式式概念的作用。

避免變異資料(不變性)

當一個函式突變或改變一個全域性變數時,會導致我們的程式出現錯誤。

在函數語言程式設計中,我們將陣列和物件等可變資料結構視為不可變資料。這意味著我們從不修改它們,而是在傳遞給函式時做一個拷貝,這樣函式就可以根據這個拷貝來計算它的輸出。

讓我們重新審視一下下面的程式碼。

``` const arr = [2, 4, 6];

function addElement(arr, ele) { return [...arr, ele]; }

console.log("modified data", addElement(arr, 8)); // Expected Output: [2, 4, 6, 8] console.log("original data", arr); // Expected Output: [2, 4, 6]

```

我們之前已經看過這段程式碼,但這次我們將把重點轉向不可變的函式方面。

在這裡,我們有一個函式,它只使用全域性變數的一個副本來計算輸出。我們使用ES6的傳播操作符()將現有資料複製到一個新的陣列中,然後新增新的元素。這樣一來,我們就保持了原始陣列輸入資料的不可變性,正如在結果中看到的那樣。

React如何處理易變的狀態

由於React是一個反應式庫,它必須對狀態變化做出 "反應",以保持DOM的更新。很明顯,狀態值也必須更新。

在React中,我們不直接修改狀態。相反,我們使用類元件中的setState() 方法或功能元件中的updater函式來更新狀態。

看一下我們之前的程式碼節選。

``` const handleClick = () => setCount((prev) => prev + 1);

```

在這裡,我們使用updater函式,setCount ,來更新計數。當處理像數字和字串這樣的不可改變的資料時,我們必須只將更新的值傳遞給updater函式,或者在下一個狀態依賴於上一個狀態時呼叫回撥函式。

讓我們看看另一個更新字串值的例子。

``` import { useState } from "react";

const App = () => { const [person, setPerson] = useState("");

const handleChange = (e) => { setPerson(e.target.value); };

return ( // ... ); };

export default App;

```

在這裡,為了簡潔起見,我們又刪除了一些程式碼。

上面的程式碼更新了一個表單的文字欄位,這涉及到對不可變的字串資料的處理。所以,我們必須通過將當前輸入值傳遞給updater函式來更新輸入欄位

然而,每當我們傳遞像陣列和物件這樣的易變資料時,我們必須製作一份狀態資料的副本,並根據副本計算輸出。注意,我們決不能修改原始狀態資料。

在下面的程式碼中,handleChange ,在表單中的每一個按鍵上都會觸發更新狀態變數。

``` import { useState } from "react";

const App = () => { const [person, setPerson] = useState({ fName: "", lName: "" });

const handleChange = (e) => { setPerson({ ...person, e.target.name: e.target.value }); };

return ( // ... ); };

export default App;

```

從程式碼中可以看出,我們正在處理一個可變的物件,因此,我們必須將狀態視為不可變的。同樣,我們通過使用ES6傳播操作符製作一個狀態的副本並更新受影響的屬性來做到這一點。

``` setPerson({ ...person,

});

```

還有一個改進就是確保更新器函式setPerson ,使用一個狀態變數,作為回撥函式的引數傳遞。

``` const handleChange = (e) => { setPerson((person) => ({ ...person, e.target.name: e.target.value })); };

```

現在,如果我們不遵循這個功能概念,直接對狀態進行變異,會發生什麼?很明顯,我們的應用程式會出現一個錯誤。

為了看到更清晰的畫面,再次訪問這個CodeSandbox,並暫時從函式中註釋出…person ,像這樣。

``` setPerson((person) => ({ // ...person,

}));

```

現在,通過嘗試在表單欄位中寫一些東西,文字將相互覆蓋。這是一個我們想要防止的錯誤,我們可以通過將狀態視為不可變的資料來做到這一點。

避免副作用

功能性程式設計程式碼的目的是純粹的。React中的純元件可以接收一個道具作為引數,並根據輸入的道具來計算輸出。

但有時,該元件可以進行影響和修改其範圍之外的一些狀態的計算。這些計算被稱為副作用。這些效應的例子包括資料的獲取和手動操作DOM。

這些都是我們在應用中經常執行的任務,因此,副作用是不可避免的。

下面的片段是基於我們之前的Counter 例子。

`` const Counter = ({ count }) => { document.title =Number of click: ${count}; return <h3>{Count: ${count}`}; };

```

在程式碼中,我們更新了文件的標題以反映更新的計數值。這是一個副作用,因為我們修改了不屬於該元件的DOM元素,從而使該元件不純。

直接在元件主體內執行副作用是不允許的,以避免我們的應用程式中出現不一致。相反,我們必須將這種效果與渲染邏輯隔離。React為我們提供了一個名為
的Hook
[useEffect](https://blog.logrocket.com/guide-to-react-useeffect-hook/) 來管理我們的副作用

下面的程式碼實現了這個Hook。

`` const Counter = ({ count }) => { useEffect(() => { document.title =Number of click: ${count}`; }, [count]);

return

{Count: ${count}}

; };

```

通過將副作用放在React的 [useEffect](https://codesandbox.io/s/admiring-hoover-s52eg?file=/src/App.js) Hook中,意味著我們可以輕鬆地測試和維護渲染邏輯。

React中的組合

在函數語言程式設計中,組合是一種通過組合或連鎖多個較小的函式來構建複雜函式的行為。

如果我們回憶一下本文的開頭,我們提到對於一個給定的函式,cf ,我們可以將它們組合成一個更復雜的函式,像這樣演示。

``` z = c(f(x))

```

但現在,我們將在React的背景下看一下這個組合的概念。

與上述功能模式類似,我們可以在React中通過使用React的children 道具注入其他元件來構建一個複雜的元件。這個道具也允許一個元件渲染不同數量的內容,而不需要提前知道內容的情況。

這使我們可以靈活地決定元件內的內容,並定製內容以獲得所需的輸出。

實現這一概念的元件的一個很好的例子包括Hero[Sidebar](https://blog.logrocket.com/create-sidebar-menu-react/).

建立一個可重複使用的Hero 元件

假設我們想建立一個包含不同內容的Hero 元件,我們可以在我們的應用程式中的任何地方重複使用它。

我們可以像這樣開始編寫元件。

``` function Hero({ children }) { return

{children}
; }

```

這段程式碼中使用的children 道具允許我們在一個元件的開頭和結尾標籤之間注入內容;在我們的例子中,是一個Hero 元件。

所以,我們可以有這樣的東西。

```

Home Page

This is the home page description

```

現在,在<Hero> 之間的所有內容都被認為是其children 的道具,因此出現在Hero 元件的section 標籤之間。

同樣地,<Banner> JSX標籤內的內容作為children prop進入Banner 元件。

``` function Banner({ children }) { return (

{children}
); }

```

<Banner> 標籤之間的內容(即h1p )在JSX中取代了children

在這段程式碼中,Banner 元件只知道button 元素,因為我們已經手動添加了該元素;它不知道什麼會取代children 的道具。

這使得該元件可以重複使用,並且可以靈活地進行定製,因為我們可以控制作為children 的內容。我們現在可以決定不在我們應用程式的另一個頁面上渲染橫幅標題,h1

我們所要做的就是把它從內容中排除 ,在 [Banner](https://codesandbox.io/s/angry-chebyshev-69hiw?file=/src/App.js) 標籤中。

通過比較React組合和數學定義,我們可以說Banner 元件的輸出成為Hero 元件的輸入。換句話說,HeroBanner 元件組成了一個完整的元件。

這就是實踐中的組合。

總結

我很高興你在這裡!在這篇文章中,我們通過實際的例子瞭解到React是如何應用函數語言程式設計的概念來做一些決定的。

我希望你喜歡閱讀這篇文章。如果你有問題或貢獻,請在評論區分享你的想法,我將很樂意招待他們。最後,努力在網路上分享這份指南。

The post Fundamentalsof functional programming with Reactappeared first onLogRocket Blog.